From f58cec9ed7be1baf66b8d25eacf413959cf22a81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Wei=C3=9Fe?= Date: Mon, 4 Sep 2023 15:28:16 +0200 Subject: [PATCH] Save Helm charts to disk before upgrading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Weiße --- cli/internal/cmd/init_test.go | 4 ++ cli/internal/cmd/upgradeapply.go | 6 ++ cli/internal/helm/BUILD.bazel | 2 + cli/internal/helm/action.go | 100 ++++++++++++++++++++++++++++++- cli/internal/helm/helm.go | 23 ++++++- 5 files changed, 131 insertions(+), 4 deletions(-) diff --git a/cli/internal/cmd/init_test.go b/cli/internal/cmd/init_test.go index af0ecbc0cfb..13ae7eb4a6e 100644 --- a/cli/internal/cmd/init_test.go +++ b/cli/internal/cmd/init_test.go @@ -232,6 +232,10 @@ func (s stubRunner) Apply(_ context.Context) error { return nil } +func (s stubRunner) SaveCharts(_ string, _ file.Handler) error { + return nil +} + func TestGetLogs(t *testing.T) { someErr := errors.New("failed") diff --git a/cli/internal/cmd/upgradeapply.go b/cli/internal/cmd/upgradeapply.go index 7ed02347320..a944bb07fad 100644 --- a/cli/internal/cmd/upgradeapply.go +++ b/cli/internal/cmd/upgradeapply.go @@ -422,6 +422,12 @@ func (u *upgradeApplyCmd) handleServiceUpgrade( } } + // Save the Helm charts for the upgrade to disk + if err := executor.SaveCharts(upgradeDir, u.fileHandler); err != nil { + return fmt.Errorf("saving Helm charts: %w", err) + } + u.log.Debugf("Helm charts saved to %s", upgradeDir) + if includesUpgrades { u.log.Debugf("Creating backup of CRDs and CRs") crds, err := u.kubeUpgrader.BackupCRDs(cmd.Context(), upgradeDir) diff --git a/cli/internal/helm/BUILD.bazel b/cli/internal/helm/BUILD.bazel index 663ca1951ee..5cecd3f9a05 100644 --- a/cli/internal/helm/BUILD.bazel +++ b/cli/internal/helm/BUILD.bazel @@ -429,6 +429,7 @@ go_library( "//internal/compatibility", "//internal/config", "//internal/constants", + "//internal/file", "//internal/kms/uri", "//internal/kubernetes/kubectl", "//internal/retry", @@ -444,6 +445,7 @@ go_library( "@sh_helm_helm_v3//pkg/action", "@sh_helm_helm_v3//pkg/chart", "@sh_helm_helm_v3//pkg/chart/loader", + "@sh_helm_helm_v3//pkg/chartutil", "@sh_helm_helm_v3//pkg/cli", "@sh_helm_helm_v3//pkg/release", ], diff --git a/cli/internal/helm/action.go b/cli/internal/helm/action.go index 5e34b28abc6..1888326a137 100644 --- a/cli/internal/helm/action.go +++ b/cli/internal/helm/action.go @@ -9,21 +9,28 @@ package helm import ( "context" "fmt" + "path/filepath" "time" "github.com/edgelesssys/constellation/v2/internal/constants" + "github.com/edgelesssys/constellation/v2/internal/file" "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chartutil" "helm.sh/helm/v3/pkg/cli" ) const ( // timeout is the maximum time given per helm action. - timeout = 10 * time.Minute + timeout = 10 * time.Minute + installChartDir = "helm-install" + upgradeChartDir = "helm-upgrade" ) type applyAction interface { Apply(context.Context) error + SaveChart(upgradeDir string, fileHandler file.Handler) error ReleaseName() string IsAtomic() bool } @@ -94,6 +101,11 @@ func (a *installAction) Apply(ctx context.Context) error { return nil } +// SaveChart saves the chart to the given directory under the `install/` subdirectory. +func (a *installAction) SaveChart(upgradeDir string, fileHandler file.Handler) error { + return saveChart(a.release, filepath.Join(upgradeDir, installChartDir), fileHandler) +} + func (a *installAction) apply(ctx context.Context) error { _, err := a.helmAction.RunWithContext(ctx, a.release.Chart, a.release.Values) return err @@ -145,6 +157,11 @@ func (a *upgradeAction) Apply(ctx context.Context) error { return nil } +// SaveChart saves the chart to the given directory under the `upgrade/` subdirectory. +func (a *upgradeAction) SaveChart(upgradeDir string, fileHandler file.Handler) error { + return saveChart(a.release, filepath.Join(upgradeDir, upgradeChartDir), fileHandler) +} + func (a *upgradeAction) apply(ctx context.Context) error { _, err := a.helmAction.RunWithContext(ctx, a.release.ReleaseName, a.release.Chart, a.release.Values) return err @@ -159,3 +176,84 @@ func (a *upgradeAction) ReleaseName() string { func (a *upgradeAction) IsAtomic() bool { return a.helmAction.Atomic } + +func saveChart(release Release, actionDir string, fileHandler file.Handler) error { + if err := saveChartToDisk(release.Chart, actionDir, fileHandler); err != nil { + return fmt.Errorf("saving chart %s to %q: %w", release.ReleaseName, actionDir, err) + } + if err := fileHandler.WriteYAML(filepath.Join(actionDir, release.Chart.Metadata.Name, "overrides.yaml"), release.Values); err != nil { + return fmt.Errorf("saving override values for chart %s to %q: %w", release.ReleaseName, actionDir, err) + } + + return nil +} + +// saveChartToDisk saves a chart as files in a directory. +// +// This takes the chart name, and creates a new subdirectory inside of the given dest +// directory, writing the chart's contents to that subdirectory. +// Dependencies are written using the same format, instead of writing them as tar files +// +// View the SaveDir implementation in chartutil as reference: https://github.com/helm/helm/blob/3a31588ad33fe3b89af5a2a54ee1d25bfe6eaa5e/pkg/chartutil/save.go#L40 +func saveChartToDisk(c *chart.Chart, dest string, fileHandler file.Handler) error { + // Create the chart directory + outdir := filepath.Join(dest, c.Name()) + if fi, err := fileHandler.Stat(outdir); err == nil && !fi.IsDir() { + return fmt.Errorf("file %s already exists and is not a directory", outdir) + } + if err := fileHandler.MkdirAll(outdir); err != nil { + return err + } + + // Save the chart file. + if err := chartutil.SaveChartfile(filepath.Join(outdir, chartutil.ChartfileName), c.Metadata); err != nil { + return err + } + + // Save values.yaml + for _, f := range c.Raw { + if f.Name == chartutil.ValuesfileName { + vf := filepath.Join(outdir, chartutil.ValuesfileName) + if err := writeFile(vf, f.Data, fileHandler); err != nil { + return err + } + } + } + + // Save values.schema.json if it exists + if c.Schema != nil { + filename := filepath.Join(outdir, chartutil.SchemafileName) + if err := writeFile(filename, c.Schema, fileHandler); err != nil { + return err + } + } + + // Save templates and files + for _, o := range [][]*chart.File{c.Templates, c.Files} { + for _, f := range o { + n := filepath.Join(outdir, f.Name) + if err := writeFile(n, f.Data, fileHandler); err != nil { + return err + } + } + } + + // Save dependencies + base := filepath.Join(outdir, chartutil.ChartsDir) + for _, dep := range c.Dependencies() { + // Don't write dependencies as tar files + // Instead recursively use saveChartToDisk + if err := saveChartToDisk(dep, base, fileHandler); err != nil { + return fmt.Errorf("saving %s: %w", dep.ChartFullPath(), err) + } + } + + return nil +} + +func writeFile(name string, content []byte, fileHandler file.Handler) error { + if err := fileHandler.MkdirAll(filepath.Dir(name)); err != nil { + return err + } + return fileHandler.Write(name, content) +} diff --git a/cli/internal/helm/helm.go b/cli/internal/helm/helm.go index 261b431e234..0d5efb0e0e8 100644 --- a/cli/internal/helm/helm.go +++ b/cli/internal/helm/helm.go @@ -36,6 +36,7 @@ import ( "github.com/edgelesssys/constellation/v2/cli/internal/terraform" "github.com/edgelesssys/constellation/v2/internal/config" "github.com/edgelesssys/constellation/v2/internal/constants" + "github.com/edgelesssys/constellation/v2/internal/file" "github.com/edgelesssys/constellation/v2/internal/kms/uri" "github.com/edgelesssys/constellation/v2/internal/kubernetes/kubectl" "github.com/edgelesssys/constellation/v2/internal/semver" @@ -51,7 +52,6 @@ const ( type debugLog interface { Debugf(format string, args ...any) - Sync() } // Client is a Helm client to apply charts. @@ -87,7 +87,10 @@ type Options struct { // PrepareApply loads the charts and returns the executor to apply them. // TODO(elchead): remove validK8sVersion by putting ValidK8sVersion into config.Config, see AB#3374. -func (h Client) PrepareApply(conf *config.Config, validK8sversion versions.ValidK8sVersion, idFile clusterid.File, flags Options, tfOutput terraform.ApplyOutput, serviceAccURI string, masterSecret uri.MasterSecret) (Applier, bool, error) { +func (h Client) PrepareApply( + conf *config.Config, validK8sversion versions.ValidK8sVersion, idFile clusterid.File, + flags Options, tfOutput terraform.ApplyOutput, serviceAccURI string, masterSecret uri.MasterSecret, +) (Applier, bool, error) { releases, err := h.loadReleases(conf, masterSecret, validK8sversion, idFile, flags, tfOutput, serviceAccURI) if err != nil { return nil, false, fmt.Errorf("loading Helm releases: %w", err) @@ -97,7 +100,10 @@ func (h Client) PrepareApply(conf *config.Config, validK8sversion versions.Valid return &ChartApplyExecutor{actions: actions, log: h.log}, includesUpgrades, err } -func (h Client) loadReleases(conf *config.Config, secret uri.MasterSecret, validK8sVersion versions.ValidK8sVersion, idFile clusterid.File, flags Options, tfOutput terraform.ApplyOutput, serviceAccURI string) ([]Release, error) { +func (h Client) loadReleases( + conf *config.Config, secret uri.MasterSecret, validK8sVersion versions.ValidK8sVersion, + idFile clusterid.File, flags Options, tfOutput terraform.ApplyOutput, serviceAccURI string, +) ([]Release, error) { helmLoader := newLoader(conf, idFile, validK8sVersion, h.cliVersion) h.log.Debugf("Created new Helm loader") return helmLoader.loadReleases(flags.Conformance, flags.HelmWaitMode, secret, @@ -107,6 +113,7 @@ func (h Client) loadReleases(conf *config.Config, secret uri.MasterSecret, valid // Applier runs the Helm actions. type Applier interface { Apply(ctx context.Context) error + SaveCharts(upgradeDir string, fileHandler file.Handler) error } // ChartApplyExecutor is a Helm action executor that applies all actions. @@ -126,6 +133,16 @@ func (c ChartApplyExecutor) Apply(ctx context.Context) error { return nil } +// SaveCharts saves all Helm charts and their values to the given directory. +func (c ChartApplyExecutor) SaveCharts(upgradeDir string, fileHandler file.Handler) error { + for _, action := range c.actions { + if err := action.SaveChart(upgradeDir, fileHandler); err != nil { + return fmt.Errorf("saving chart %s: %w", action.ReleaseName(), err) + } + } + return nil +} + // mergeMaps returns a new map that is the merger of it's inputs. // Key collisions are resolved by taking the value of the second argument (map b). // Taken from: https://github.com/helm/helm/blob/dbc6d8e20fe1d58d50e6ed30f09a04a77e4c68db/pkg/cli/values/options.go#L91-L108.