diff --git a/internal/cli/local/setup/run.go b/internal/cli/local/setup/run.go index cd0774f..32c2ca1 100644 --- a/internal/cli/local/setup/run.go +++ b/internal/cli/local/setup/run.go @@ -131,5 +131,5 @@ func runSetup(cmd *cobra.Command, args []string) error { Override: chartLocation, } - return chart.InstallOrUpgrade(cmd.Context(), chartConfig, helmOptions) + return chart.Install(cmd.Context(), chartConfig, helmOptions) } diff --git a/internal/cli/local/upgrade/run.go b/internal/cli/local/upgrade/run.go index dd84588..f841f3c 100644 --- a/internal/cli/local/upgrade/run.go +++ b/internal/cli/local/upgrade/run.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "strings" + "time" "github.com/spf13/cobra" @@ -64,15 +65,18 @@ func runUpgrade(cmd *cobra.Command, args []string) error { } } - err := k3s.Upgrade(cmd.Context(), config.K3S) + err := k3s.Upgrade(cmd.Context(), k3s.UpgradeConfig{Debug: config.Debug}) if err != nil { return err } - if len(k3sImportConfig.ImagePaths) == 0 { - err = k3s.ImportBundleImages(cmd.Context(), k3sImportConfig.FailFast) + time.Sleep(5 * time.Second) // wait for k3s to be ready + + // in debug mode we don't do fail-fast + if len(config.Images.ImportPaths) == 0 { + err = k3s.ImportBundleImages(cmd.Context(), !config.Debug) } else { - err = k3s.ImportImages(cmd.Context(), k3sImportConfig.ImagePaths, k3sImportConfig.FailFast) + err = k3s.ImportImages(cmd.Context(), config.Images.ImportPaths, !config.Debug) } if err != nil { @@ -119,5 +123,5 @@ func runUpgrade(cmd *cobra.Command, args []string) error { Override: chartLocation, } - return chart.InstallOrUpgrade(cmd.Context(), chartConfig, helmOptions) + return chart.Upgrade(cmd.Context(), chartConfig, helmOptions, config.Debug) } diff --git a/internal/cli/local/upgrade/upgrade.go b/internal/cli/local/upgrade/upgrade.go index 21a52a0..e0d7367 100644 --- a/internal/cli/local/upgrade/upgrade.go +++ b/internal/cli/local/upgrade/upgrade.go @@ -5,7 +5,6 @@ import ( "github.com/weka/gohomecli/internal/cli/app/hooks" "github.com/weka/gohomecli/internal/install/bundle" - "github.com/weka/gohomecli/internal/install/k3s" "github.com/weka/gohomecli/internal/install/web" "github.com/weka/gohomecli/internal/utils" ) @@ -19,19 +18,17 @@ var config struct { Web bool WebBindAddr string BundlePath string - K3S k3s.UpgradeConfig - Chart struct { + Images struct { + ImportPaths []string + } + Chart struct { kubeConfigPath string localChart string jsonConfig string remoteDownload bool remoteVersion string } -} - -var k3sImportConfig struct { - ImagePaths []string - FailFast bool + Debug bool } var upgradeCmd = &cobra.Command{ @@ -51,13 +48,12 @@ func init() { } upgradeCmd.Flags().StringVar(&config.BundlePath, "bundle", bundle.BundlePath(), "bundle directory with k3s package") - upgradeCmd.Flags().BoolVar(&config.K3S.Debug, "debug", false, "enable debug mode") + upgradeCmd.Flags().BoolVar(&config.Debug, "debug", false, "enable debug mode") upgradeCmd.Flags().MarkHidden("bundle") upgradeCmd.Flags().MarkHidden("debug") - upgradeCmd.Flags().BoolVar(&k3sImportConfig.FailFast, "fail-fast", false, "fail on first error") - upgradeCmd.Flags().StringSliceVarP(&k3sImportConfig.ImagePaths, "image-path", "f", nil, "images to import (if specified, bundle images are ignored)") + upgradeCmd.Flags().StringSliceVarP(&config.Images.ImportPaths, "image-path", "f", nil, "images to import (if specified, bundle images are ignored)") upgradeCmd.Flags().StringVarP(&config.Chart.kubeConfigPath, "kube-config", "k", "/etc/rancher/k3s/k3s.yaml", "Path to kubeconfig file") upgradeCmd.Flags().StringVarP(&config.Chart.localChart, "local-chart", "l", "", "Path to local chart directory/archive") diff --git a/internal/install/chart/chart.go b/internal/install/chart/chart.go new file mode 100644 index 0000000..8e2f3d7 --- /dev/null +++ b/internal/install/chart/chart.go @@ -0,0 +1,176 @@ +package chart + +import ( + "fmt" + "os" + "path/filepath" + "time" + + helmclient "github.com/mittwald/go-helm-client" + "gopkg.in/yaml.v3" + "helm.sh/helm/v3/pkg/repo" + + "github.com/weka/gohomecli/internal/install/bundle" + "github.com/weka/gohomecli/internal/utils" +) + +const ( + ReleaseName = "wekahome" + ReleaseNamespace = "home-weka-io" + RepositoryURL = "https://weka.github.io/gohome" + RepositoryName = "wekahome" + ChartName = "wekahome" +) + +var ErrUnableToFindChart = fmt.Errorf("unable to determine chart location") + +var logger = utils.GetLogger("HelmChart") + +type LocationOverride struct { + Path string // path to chart package + RemoteDownload bool // download from remote repository + Version string // version of the chart to download from remote repository +} + +type HelmOptions struct { + KubeConfig []byte // path or content of kubeconfig file + Override *LocationOverride // override chart package location + KubeContext string // kubeconfig context to use + NamespaceOverride string // override namespace for release +} + +// Configuration flat options for the chart, pointers are used to distinguish between empty and unset values +type Configuration struct { + Host *string `json:"host"` // ingress host + TLS *bool `json:"tls"` // ingress tls enabled + TLSCert *string `json:"tlsCert"` // ingress tls cert + TLSKey *string `json:"tlsKey"` // ingress tls key + + SMTPHost *string `json:"smtpHost"` // smtp server host + SMTPPort *int `json:"smtpPort"` // smtp server port + SMTPUser *string `json:"smtpUser"` // smtp server user + SMTPPassword *string `json:"smtpPassword"` // smtp server password + SMTPInsecure *bool `json:"smtpInsecure"` // smtp insecure connection + SMTPSender *string `json:"smtpSender"` // smtp sender name + SMTPSenderEmail *string `json:"smtpSenderEmail"` // smtp sender email + + DiagnosticsRetentionDays *int `json:"diagnosticsRetentionDays"` // diagnostics retention days + EventsRetentionDays *int `json:"eventsRetentionDays"` // events retention days + + ForwardingEnabled bool `json:"forwardingEnabled"` // forwarding enabled + ForwardingUrl *string `json:"forwardingUrl"` // forwarding url override + ForwardingEnableEvents *bool `json:"forwardingEnableEvents"` // forwarding enable events + ForwardingEnableUsageReports *bool `json:"forwardingEnableUsageReports"` // forwarding enable usage reports + ForwardingEnableAnalytics *bool `json:"forwardingEnableAnalytics"` // forwarding enable analytics + ForwardingEnableDiagnostics *bool `json:"forwardingEnableDiagnostics"` // forwarding enable diagnostics + ForwardingEnableStats *bool `json:"forwardingEnableStats"` // forwarding enable stats + ForwardingEnableClusterRegistration *bool `json:"forwardingEnableClusterRegistration"` // forwarding enable cluster registration + + Autoscaling bool `json:"autoscaling"` // enable services autoscaling + WekaNodesServed *int `json:"wekaNodesMonitored"` // number of weka nodes to monitor, controls load preset +} + +func chartSpec(client helmclient.Client, cfg *Configuration, opts *HelmOptions) (*helmclient.ChartSpec, error) { + namespace := ReleaseNamespace + if opts.NamespaceOverride != "" { + namespace = opts.NamespaceOverride + } + + logger.Debug(). + Interface("locationOverride", opts.Override). + Msg("Determining chart location") + chartLocation, err := getChartLocation(client, opts) + if err != nil { + return nil, err + } + + logger.Debug(). + Interface("configuration", cfg). + Msg("Generating chart values") + + values, err := generateValuesV3(cfg) + if err != nil { + return nil, err + } + + valuesYaml, err := yaml.Marshal(values) + if err != nil { + return nil, fmt.Errorf("failed serializing values yaml: %w", err) + } + logger.Debug().Msgf("Generated values:\n %s", string(valuesYaml)) + + chartVersion := "" // any available + if opts.Override != nil { + chartVersion = opts.Override.Version + } + return &helmclient.ChartSpec{ + ReleaseName: ReleaseName, + ChartName: chartLocation, + Version: chartVersion, + Namespace: namespace, + ValuesYaml: string(valuesYaml), + CreateNamespace: true, + ResetValues: true, + Wait: true, + WaitForJobs: true, + Timeout: time.Minute * 5, + }, nil +} + +func findBundledChart() (string, error) { + path := "" + + err := bundle.Walk("", func(name string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if path != "" { + return nil + } + + matched, err := filepath.Match("wekahome-*.tgz", info.Name()) + if err != nil { + return err + } + + if matched { + path = name + } + + return nil + }) + + if err != nil || path == "" { + return "", fmt.Errorf("unable to find wekahome chart in bundle") + } + + return path, nil +} + +func getChartLocation(client helmclient.Client, opts *HelmOptions) (string, error) { + var chartLocation string + + if opts.Override != nil && opts.Override.RemoteDownload { + err := client.AddOrUpdateChartRepo(repo.Entry{ + Name: RepositoryName, + URL: RepositoryURL, + }) + if err != nil { + return "", fmt.Errorf("failed adding chart repo: %w", err) + } + + chartLocation = fmt.Sprintf("%s/%s", RepositoryName, ChartName) + return chartLocation, nil + } + + if opts.Override != nil && opts.Override.Path != "" { + return opts.Override.Path, nil + } + + if bundle.IsBundled() { + return findBundledChart() + } + + return "", ErrUnableToFindChart +} diff --git a/internal/install/chart/configuration.go b/internal/install/chart/configuration.go deleted file mode 100644 index de3015b..0000000 --- a/internal/install/chart/configuration.go +++ /dev/null @@ -1,32 +0,0 @@ -package chart - -// Configuration flat options for the chart, pointers are used to distinguish between empty and unset values -type Configuration struct { - Host *string `json:"host"` // ingress host - TLS *bool `json:"tls"` // ingress tls enabled - TLSCert *string `json:"tlsCert"` // ingress tls cert - TLSKey *string `json:"tlsKey"` // ingress tls key - - SMTPHost *string `json:"smtpHost"` // smtp server host - SMTPPort *int `json:"smtpPort"` // smtp server port - SMTPUser *string `json:"smtpUser"` // smtp server user - SMTPPassword *string `json:"smtpPassword"` // smtp server password - SMTPInsecure *bool `json:"smtpInsecure"` // smtp insecure connection - SMTPSender *string `json:"smtpSender"` // smtp sender name - SMTPSenderEmail *string `json:"smtpSenderEmail"` // smtp sender email - - DiagnosticsRetentionDays *int `json:"diagnosticsRetentionDays"` // diagnostics retention days - EventsRetentionDays *int `json:"eventsRetentionDays"` // events retention days - - ForwardingEnabled bool `json:"forwardingEnabled"` // forwarding enabled - ForwardingUrl *string `json:"forwardingUrl"` // forwarding url override - ForwardingEnableEvents *bool `json:"forwardingEnableEvents"` // forwarding enable events - ForwardingEnableUsageReports *bool `json:"forwardingEnableUsageReports"` // forwarding enable usage reports - ForwardingEnableAnalytics *bool `json:"forwardingEnableAnalytics"` // forwarding enable analytics - ForwardingEnableDiagnostics *bool `json:"forwardingEnableDiagnostics"` // forwarding enable diagnostics - ForwardingEnableStats *bool `json:"forwardingEnableStats"` // forwarding enable stats - ForwardingEnableClusterRegistration *bool `json:"forwardingEnableClusterRegistration"` // forwarding enable cluster registration - - Autoscaling bool `json:"autoscaling"` // enable services autoscaling - WekaNodesServed *int `json:"wekaNodesMonitored"` // number of weka nodes to monitor, controls load preset -} diff --git a/internal/install/chart/install.go b/internal/install/chart/install.go index 9c79810..da1e398 100644 --- a/internal/install/chart/install.go +++ b/internal/install/chart/install.go @@ -4,49 +4,13 @@ import ( "context" "errors" "fmt" - "os" - "path/filepath" - "time" - - "github.com/weka/gohomecli/internal/install/bundle" helmclient "github.com/mittwald/go-helm-client" - "gopkg.in/yaml.v3" - "helm.sh/helm/v3/pkg/repo" "github.com/weka/gohomecli/internal/utils" ) -const ( - ReleaseName = "wekahome" - ReleaseNamespace = "home-weka-io" - RepositoryURL = "https://weka.github.io/gohome" - RepositoryName = "wekahome" - ChartName = "wekahome" -) - -var ErrUnableToFindChart = fmt.Errorf("unable to determine chart location") - -var logger = utils.GetLogger("HelmChart") - -type LocationOverride struct { - Path string // path to chart package - RemoteDownload bool // download from remote repository - Version string // version of the chart to download from remote repository -} - -type HelmOptions struct { - KubeConfig []byte // path or content of kubeconfig file - Override *LocationOverride // override chart package location - KubeContext string // kubeconfig context to use - NamespaceOverride string // override namespace for release -} - -func InstallOrUpgrade( - ctx context.Context, - cfg *Configuration, - opts *HelmOptions, -) error { +func Install(ctx context.Context, cfg *Configuration, opts *HelmOptions) error { namespace := ReleaseNamespace if opts.NamespaceOverride != "" { namespace = opts.NamespaceOverride @@ -75,120 +39,27 @@ func InstallOrUpgrade( return fmt.Errorf("failed configuring helm client: %w", err) } - logger.Debug(). - Interface("locationOverride", opts.Override). - Msg("Determining chart location") - chartLocation, err := getChartLocation(client, opts) - if err != nil { - return err - } - - logger.Debug(). - Interface("configuration", cfg). - Msg("Generating chart values") - - values, err := generateValuesV3(cfg) - if err != nil { - return err - } - - valuesYaml, err := yaml.Marshal(values) + spec, err := chartSpec(client, cfg, opts) if err != nil { - return fmt.Errorf("failed serializing values yaml: %w", err) - } - logger.Debug().Msgf("Generated values:\n %s", string(valuesYaml)) - - chartVersion := "" // any available - if opts.Override != nil { - chartVersion = opts.Override.Version - } - chartSpec := &helmclient.ChartSpec{ - ReleaseName: ReleaseName, - ChartName: chartLocation, - Version: chartVersion, - Namespace: namespace, - ValuesYaml: string(valuesYaml), - CreateNamespace: true, - ResetValues: true, - Wait: true, - WaitForJobs: true, - Timeout: time.Minute * 5, + return fmt.Errorf("failed to prepare chart spec: %w", err) } logger.Info(). - Str("namespace", namespace). - Str("chart", chartSpec.ChartName). - Str("release", chartSpec.ReleaseName). - Msg("Installing/upgrading chart") + Str("namespace", spec.Namespace). + Str("chart", spec.ChartName). + Str("release", spec.ReleaseName). + Msg("Installing chart") - release, err := client.InstallOrUpgradeChart(ctx, chartSpec, nil) + release, err := client.InstallChart(ctx, spec, nil) if err != nil { if errors.Is(err, context.Canceled) { logger.Info().Msg("Chart installation was cancelled") return nil } - return fmt.Errorf("failed installing/upgrading chart: %w", err) + return fmt.Errorf("failed installing chart: %w", err) } logger.Info().Msg(release.Info.Notes) return nil } - -func findBundledChart() (string, error) { - path := "" - - err := bundle.Walk("", func(name string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - if path != "" { - return nil - } - - matched, err := filepath.Match("wekahome-*.tgz", info.Name()) - if err != nil { - return err - } - - if matched { - path = name - } - - return nil - }) - - if err != nil || path == "" { - return "", fmt.Errorf("unable to find wekahome chart in bundle") - } - - return path, nil -} - -func getChartLocation(client helmclient.Client, opts *HelmOptions) (string, error) { - var chartLocation string - - if opts.Override != nil && opts.Override.RemoteDownload { - err := client.AddOrUpdateChartRepo(repo.Entry{ - Name: RepositoryName, - URL: RepositoryURL, - }) - if err != nil { - return "", fmt.Errorf("failed adding chart repo: %w", err) - } - - chartLocation = fmt.Sprintf("%s/%s", RepositoryName, ChartName) - return chartLocation, nil - } - - if opts.Override != nil && opts.Override.Path != "" { - return opts.Override.Path, nil - } - - if bundle.IsBundled() { - return findBundledChart() - } - - return "", ErrUnableToFindChart -} diff --git a/internal/install/chart/upgrade.go b/internal/install/chart/upgrade.go new file mode 100644 index 0000000..7248672 --- /dev/null +++ b/internal/install/chart/upgrade.go @@ -0,0 +1,66 @@ +package chart + +import ( + "context" + "fmt" + + helmclient "github.com/mittwald/go-helm-client" + "github.com/weka/gohomecli/internal/utils" +) + +func Upgrade(ctx context.Context, cfg *Configuration, opts *HelmOptions, debug bool) error { + namespace := ReleaseNamespace + if opts.NamespaceOverride != "" { + namespace = opts.NamespaceOverride + } + + logger.Info(). + Str("namespace", namespace). + Str("kubeContext", opts.KubeContext). + Msg("Configuring helm client") + + // kubeContext override isn't working - https://github.com/mittwald/go-helm-client/issues/127 + client, err := helmclient.NewClientFromKubeConf(&helmclient.KubeConfClientOptions{ + Options: &helmclient.Options{ + Namespace: namespace, + DebugLog: func(format string, v ...interface{}) { + logger.Debug().Msgf(format, v...) + }, + Output: utils.NewWriteScanner(func(b []byte) { + logger.Info().Msg(string(b)) + }), + }, + KubeContext: opts.KubeContext, + KubeConfig: opts.KubeConfig, + }) + if err != nil { + return fmt.Errorf("failed configuring helm client: %w", err) + } + + spec, err := chartSpec(client, cfg, opts) + if err != nil { + return fmt.Errorf("failed to prepare chart spec: %w", err) + } + + logger.Info(). + Str("namespace", spec.Namespace). + Str("chart", spec.ChartName). + Str("release", spec.ReleaseName). + Msg("Upgrading chart") + + release, err := client.UpgradeChart(ctx, spec, nil) + if err != nil { + if !debug { + if err := client.RollbackRelease(spec); err != nil { + logger.Error().Err(err).Msg("Rollback failed") + } + } + + logger.Error().Err(err).Msg("Upgrade failed") + return err + } + + logger.Info().Msg(release.Info.Notes) + + return nil +} diff --git a/internal/install/k3s/backup.go b/internal/install/k3s/backup.go new file mode 100644 index 0000000..d0e53f4 --- /dev/null +++ b/internal/install/k3s/backup.go @@ -0,0 +1,114 @@ +package k3s + +import ( + "fmt" + "io" + "os" + "path/filepath" +) + +type backedupFile struct { + Filename string + Filemode os.FileMode + Backup string +} + +func backupK3S() ([]backedupFile, error) { + tmp, err := os.MkdirTemp("/tmp", "homecli_backup") + if err != nil { + return nil, fmt.Errorf("mktemp: %w", err) + } + + matches, err := filepath.Glob(filepath.Join(k3sImagesPath, "k3s-airgap-images-*.tar.gz")) + if err != nil { + return nil, fmt.Errorf("airgap images: %w", err) + } + matches = append(matches, k3sBinary()) + + logger.Info().Interface("files", matches).Msgf("Backing up files to %q", tmp) + + var results = make([]backedupFile, 0, len(matches)) + + for _, fname := range matches { + result, err := backupFile(tmp, fname) + if err != nil { + logger.Error().Err(err).Msg("Backup failed") + return results, err + } + results = append(results, result) + } + + logger.Info().Msg("Backup done") + + return results, err +} + +func backupFile(tmp string, fname string) (backedupFile, error) { + var result = backedupFile{ + Filename: fname, + } + + stat, err := os.Stat(fname) + if err != nil { + return result, fmt.Errorf("stat %q: %w", fname, err) + } + + result.Filemode = stat.Mode() + + file, err := os.Open(fname) + if err != nil { + return result, fmt.Errorf("open %q: %w", fname, err) + } + defer file.Close() + + result.Backup = filepath.Join(tmp, filepath.Base(fname)) + + tmpfile, err := os.OpenFile(result.Backup, os.O_CREATE|os.O_WRONLY, stat.Mode()) + if err != nil { + return result, fmt.Errorf("open tmpfile: %w", err) + } + defer tmpfile.Close() + + _, err = io.Copy(tmpfile, file) + if err != nil { + return result, fmt.Errorf("copy: %w", err) + } + + return result, nil +} + +func restore(files []backedupFile) error { + logger.Info().Msg("Restoring from backup") + + for _, file := range files { + if err := restoreFile(file); err != nil { + logger.Error(). + Err(err).Interface("files", files). + Msg("Restoring from backup failed due to error, please copy files manually and restart the service") + return err + } + } + + logger.Info().Msg("Original files was restored") + return nil +} + +func restoreFile(file backedupFile) error { + tmpfile, err := os.Open(file.Backup) + if err != nil { + return fmt.Errorf("open %q: %w", file.Backup, err) + } + defer tmpfile.Close() + + restored, err := os.OpenFile(file.Filename, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, file.Filemode) + if err != nil { + return fmt.Errorf("open %q: %w", file.Filename, err) + } + defer restored.Close() + + _, err = io.Copy(restored, tmpfile) + if err != nil { + return fmt.Errorf("copy: %w", err) + } + return nil +} diff --git a/internal/install/k3s/images.go b/internal/install/k3s/images.go index 03e1b70..07cdd69 100644 --- a/internal/install/k3s/images.go +++ b/internal/install/k3s/images.go @@ -1,7 +1,6 @@ package k3s import ( - "bytes" "compress/gzip" "context" "errors" @@ -19,36 +18,36 @@ func getCurrentPlatform() string { return fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH) } -func unzippedData(imagePath string) ([]byte, error) { +func imageReader(imagePath string) (r io.Reader, close func() error, err error) { file, err := os.Open(imagePath) if err != nil { - return nil, err + return nil, nil, err } - defer file.Close() - buffer := make([]byte, 512) _, err = file.Read(buffer) if err != nil { - return nil, err + return nil, nil, err } _, err = file.Seek(0, 0) if err != nil { - return nil, err + return nil, nil, err } mime := http.DetectContentType(buffer) if mime == "application/x-gzip" { reader, err := gzip.NewReader(file) if err != nil { - return nil, err + return nil, nil, err } - return io.ReadAll(reader) + return reader, func() error { + return errors.Join(reader.Close(), file.Close()) + }, nil } - return io.ReadAll(file) + return file, file.Close, nil } func ImportImages(ctx context.Context, imagePaths []string, failFast bool) error { @@ -63,7 +62,7 @@ func ImportImages(ctx context.Context, imagePaths []string, failFast bool) error default: } - data, err := unzippedData(imagePath) + reader, closeFn, err := imageReader(imagePath) if err != nil { logger.Warn(). Err(err). @@ -77,11 +76,15 @@ func ImportImages(ctx context.Context, imagePaths []string, failFast bool) error } } + if closeFn != nil { + defer closeFn() + } + cmd, err := utils.ExecCommand(ctx, "k3s", []string{ "ctr", "image", "import", "--platform", getCurrentPlatform(), "--", "-"}, - utils.WithStdin(bytes.NewBuffer(data)), + utils.WithStdin(reader), utils.WithStdoutLogger(logger, utils.InfoLevel), utils.WithStderrLogger(logger, utils.WarnLevel), ) diff --git a/internal/install/k3s/install.go b/internal/install/k3s/install.go index 0def473..4830033 100644 --- a/internal/install/k3s/install.go +++ b/internal/install/k3s/install.go @@ -16,14 +16,12 @@ import ( "github.com/weka/gohomecli/internal/utils" ) -const ( - k3sImagesPath = "/var/lib/rancher/k3s/agent/images/" - defaultLocalStoragePath = "/opt/local-path-provisioner" -) - -var ErrExists = errors.New("k3s already installed") +const k3sImagesPath = "/var/lib/rancher/k3s/agent/images/" -var k3sBundleRegexp = regexp.MustCompile(`k3s.*\.(tar(\.gz)?)|(tgz)`) +var ( + ErrExists = errors.New("k3s already installed") + k3sBundleRegexp = regexp.MustCompile(`k3s.*\.(tar(\.gz)?)|(tgz)`) +) type InstallConfig struct { Iface string // interface for k3s network to work on, required diff --git a/internal/install/k3s/common.go b/internal/install/k3s/k3s.go similarity index 97% rename from internal/install/k3s/common.go rename to internal/install/k3s/k3s.go index b826483..dab185c 100644 --- a/internal/install/k3s/common.go +++ b/internal/install/k3s/k3s.go @@ -21,7 +21,10 @@ import ( "github.com/weka/gohomecli/internal/utils" ) -const k3sInstallPath = "/usr/local/bin" +const ( + k3sInstallPath = "/usr/local/bin" + defaultLocalStoragePath = "/opt/local-path-provisioner" +) var logger = utils.GetLogger("K3S") diff --git a/internal/install/k3s/upgrade.go b/internal/install/k3s/upgrade.go index 8718eda..f1537d0 100644 --- a/internal/install/k3s/upgrade.go +++ b/internal/install/k3s/upgrade.go @@ -16,7 +16,7 @@ type UpgradeConfig struct { Debug bool } -func Upgrade(ctx context.Context, c UpgradeConfig) error { +func Upgrade(ctx context.Context, c UpgradeConfig) (retErr error) { setupLogger(c.Debug) if !hasK3S() { @@ -31,14 +31,12 @@ func Upgrade(ctx context.Context, c UpgradeConfig) error { } logger.Debug().Msg("Parsing K3S version") - curVersion, err := getK3SVersion(k3sBinary()) if err != nil { return fmt.Errorf("get k3s version: %w", err) } logger.Info().Msgf("Found k3s bundle %q, current version %q\n", manifest.K3S, curVersion) - if semver.Compare(manifest.K3S, curVersion) == -1 && !c.Debug { logger.Error().Msg("Downgrading kubernetes cluster is not possible") return nil @@ -49,23 +47,32 @@ func Upgrade(ctx context.Context, c UpgradeConfig) error { return fmt.Errorf("stop K3S service: %w", err) } - logger.Info().Msg("Copying new k3s image...") - bundle := bundle.Tar(file) + backupFiles, err := backupK3S() + if err != nil { + if !c.Debug { + return fmt.Errorf("backup k3s: %w", err) + } + logger.Warn().Err(err).Msg("Backing up old K3S failed, doing upgrade anyway...") + } + defer func() { + if retErr != nil && len(backupFiles) > 0 && !c.Debug { + retErr = errors.Join(retErr, restore(backupFiles)) + } + if err := serviceCmd("start").Run(); err != nil { + retErr = errors.Join(retErr, fmt.Errorf("start K3S service: %w", err)) + } + }() - err = bundle.GetFiles(ctx, copyK3S(), copyAirgapImages()) + logger.Info().Msg("Copying new k3s image...") + err = bundle.Tar(file).GetFiles(ctx, copyK3S(), copyAirgapImages()) if err != nil { if errors.Is(err, context.Canceled) { - logger.Info().Msg("Upgrade was cancelled") - return nil + logger.Warn().Msg("Upgrade was cancelled") + return err } - return fmt.Errorf("read bundle: %w", err) } - if err := serviceCmd("start").Run(); err != nil { - return fmt.Errorf("start K3S service: %w", err) - } - logger.Info().Msg("Upgrade completed") return nil diff --git a/internal/install/web/api/chart.go b/internal/install/web/api/chart.go index 2c6a462..210e6f3 100644 --- a/internal/install/web/api/chart.go +++ b/internal/install/web/api/chart.go @@ -35,7 +35,7 @@ func installChart(w http.ResponseWriter, r *http.Request) { return } - err = chart.InstallOrUpgrade( + err = chart.Install( r.Context(), &installRequest.config, &chart.HelmOptions{KubeConfig: kubeConfig},