diff --git a/cli/internal/cmd/init_test.go b/cli/internal/cmd/init_test.go index af0ecbc0c..13ae7eb4a 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 ff52bf140..cdd2781a0 100644 --- a/cli/internal/cmd/upgradeapply.go +++ b/cli/internal/cmd/upgradeapply.go @@ -422,6 +422,13 @@ func (u *upgradeApplyCmd) handleServiceUpgrade( } } + // Save the Helm charts for the upgrade to disk + chartDir := filepath.Join(upgradeDir, "helm-charts") + if err := executor.SaveCharts(chartDir, u.fileHandler); err != nil { + return fmt.Errorf("saving Helm charts to disk: %w", err) + } + u.log.Debugf("Helm charts saved to %s", chartDir) + 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 663ca1951..ef5dd8f7e 100644 --- a/cli/internal/helm/BUILD.bazel +++ b/cli/internal/helm/BUILD.bazel @@ -6,6 +6,7 @@ go_library( srcs = [ "action.go", "actionfactory.go", + "chartutil.go", "ciliumhelper.go", "helm.go", "loader.go", @@ -429,6 +430,7 @@ go_library( "//internal/compatibility", "//internal/config", "//internal/constants", + "//internal/file", "//internal/kms/uri", "//internal/kubernetes/kubectl", "//internal/retry", @@ -444,6 +446,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 5e34b28ab..98de35266 100644 --- a/cli/internal/helm/action.go +++ b/cli/internal/helm/action.go @@ -9,9 +9,11 @@ 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/cli" @@ -24,6 +26,7 @@ const ( type applyAction interface { Apply(context.Context) error + SaveChart(chartsDir string, fileHandler file.Handler) error ReleaseName() string IsAtomic() bool } @@ -94,6 +97,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(chartsDir string, fileHandler file.Handler) error { + return saveChart(a.release, chartsDir, fileHandler) +} + func (a *installAction) apply(ctx context.Context) error { _, err := a.helmAction.RunWithContext(ctx, a.release.Chart, a.release.Values) return err @@ -145,6 +153,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(chartsDir string, fileHandler file.Handler) error { + return saveChart(a.release, chartsDir, 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 +172,14 @@ func (a *upgradeAction) ReleaseName() string { func (a *upgradeAction) IsAtomic() bool { return a.helmAction.Atomic } + +func saveChart(release Release, chartsDir string, fileHandler file.Handler) error { + if err := saveChartToDisk(release.Chart, chartsDir, fileHandler); err != nil { + return fmt.Errorf("saving chart %s to %q: %w", release.ReleaseName, chartsDir, err) + } + if err := fileHandler.WriteYAML(filepath.Join(chartsDir, release.Chart.Metadata.Name, "overrides.yaml"), release.Values); err != nil { + return fmt.Errorf("saving override values for chart %s to %q: %w", release.ReleaseName, filepath.Join(chartsDir, release.Chart.Metadata.Name), err) + } + + return nil +} diff --git a/cli/internal/helm/chartutil.go b/cli/internal/helm/chartutil.go new file mode 100644 index 000000000..405b57175 --- /dev/null +++ b/cli/internal/helm/chartutil.go @@ -0,0 +1,86 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package helm + +import ( + "fmt" + "path/filepath" + + "github.com/edgelesssys/constellation/v2/internal/file" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chartutil" +) + +// 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 261b431e2..bc0977d54 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(chartsDir 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(chartsDir string, fileHandler file.Handler) error { + for _, action := range c.actions { + if err := action.SaveChart(chartsDir, 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.