Skip to content
Merged
16 changes: 8 additions & 8 deletions src/internal/packager/helm/chart.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,12 @@ func InstallOrUpgradeChart(ctx context.Context, zarfChart v1alpha1.ZarfChart, ch
// Setup K8s connection.
actionConfig, err := createActionConfig(ctx, zarfChart.Namespace)
if err != nil {
return nil, "", fmt.Errorf("unable to initialize the K8s client: %w", err)
return nil, zarfChart.ReleaseName, fmt.Errorf("unable to initialize the K8s client: %w", err)
}

postRender, err := newRenderer(ctx, zarfChart, opts.AdoptExistingResources, opts.Cluster, opts.AirgapMode, opts.State, actionConfig, opts.VariableConfig)
if err != nil {
return nil, "", fmt.Errorf("unable to create helm renderer: %w", err)
return nil, zarfChart.ReleaseName, fmt.Errorf("unable to create helm renderer: %w", err)
}

histClient := action.NewHistory(actionConfig)
Expand Down Expand Up @@ -126,7 +126,7 @@ func InstallOrUpgradeChart(ctx context.Context, zarfChart v1alpha1.ZarfChart, ch

releases, err := histClient.Run(zarfChart.ReleaseName)
if err != nil {
return nil, "", errors.Join(err, installErr)
return nil, zarfChart.ReleaseName, errors.Join(err, installErr)
}
previouslyDeployedVersion := 0

Expand All @@ -139,21 +139,21 @@ func InstallOrUpgradeChart(ctx context.Context, zarfChart v1alpha1.ZarfChart, ch

// No prior releases means this was an initial install.
if previouslyDeployedVersion == 0 {
return nil, "", installErr
return nil, zarfChart.ReleaseName, installErr
}

// Attempt to rollback on a failed upgrade.
l.Info("performing Helm rollback", "chart", zarfChart.Name)
err = rollbackChart(zarfChart.ReleaseName, previouslyDeployedVersion, actionConfig, opts.Timeout)
if err != nil {
return nil, "", fmt.Errorf("%w: unable to rollback: %w", installErr, err)
return nil, zarfChart.ReleaseName, fmt.Errorf("%w: unable to rollback: %w", installErr, err)
}
return nil, "", installErr
return nil, zarfChart.ReleaseName, installErr
}

resourceList, err := actionConfig.KubeClient.Build(bytes.NewBufferString(release.Manifest), true)
if err != nil {
return nil, "", fmt.Errorf("unable to build the resource list: %w", err)
return nil, zarfChart.ReleaseName, fmt.Errorf("unable to build the resource list: %w", err)
}

runtimeObjs := []runtime.Object{}
Expand All @@ -164,7 +164,7 @@ func InstallOrUpgradeChart(ctx context.Context, zarfChart v1alpha1.ZarfChart, ch
// Ensure we don't go past the timeout by using a context initialized with the helm timeout
l.Info("running health checks", "chart", zarfChart.Name)
if err := healthchecks.WaitForReadyRuntime(helmCtx, opts.Cluster.Watcher, runtimeObjs); err != nil {
return nil, "", err
return nil, zarfChart.ReleaseName, err
}
}
l.Debug("done processing Helm chart", "name", zarfChart.Name, "duration", time.Since(start))
Expand Down
60 changes: 37 additions & 23 deletions src/pkg/packager/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,18 +221,30 @@ func (d *deployer) deployComponents(ctx context.Context, pkgLayout *layout.Packa
}

if deployErr != nil {
onFailure()
deployedComponents[idx].Status = state.ComponentStatusFailed
if d.isConnectedToCluster() {
if _, err := d.c.RecordPackageDeployment(ctx, pkgLayout.Pkg, deployedComponents, packageGeneration, state.WithPackageNamespaceOverride(opts.NamespaceOverride)); err != nil {
l.Debug("unable to record package deployment", "component", component.Name, "error", err.Error())
cleanup := func(ctx context.Context) {
onFailure()
l.Debug("component deployment failed", "component", component.Name, "error", deployErr.Error())
deployedComponents[idx].Status = state.ComponentStatusFailed
deployedComponents[idx].InstalledCharts = state.MergeInstalledChartsForComponent(deployedComponents[idx].InstalledCharts, charts, true)
if d.isConnectedToCluster() {
if _, err := d.c.RecordPackageDeployment(ctx, pkgLayout.Pkg, deployedComponents, packageGeneration, state.WithPackageNamespaceOverride(opts.NamespaceOverride)); err != nil {
l.Debug("unable to record package deployment", "component", component.Name, "error", err.Error())
}
}
}
return nil, fmt.Errorf("unable to deploy component %q: %w", component.Name, deployErr)
select {
case <-ctx.Done():
// Use background context here in order to ensure the cleanup logic can run when the context is cancelled
cleanup(context.Background())
return nil, fmt.Errorf("context cancelled while deploying component %q: %w", component.Name, deployErr)
default:
cleanup(ctx)
return nil, fmt.Errorf("unable to deploy component %q: %w", component.Name, deployErr)
}
}

// Update the package secret to indicate that we successfully deployed this component
deployedComponents[idx].InstalledCharts = charts
deployedComponents[idx].InstalledCharts = state.MergeInstalledChartsForComponent(deployedComponents[idx].InstalledCharts, charts, false)
deployedComponents[idx].Status = state.ComponentStatusSucceeded
if d.isConnectedToCluster() {
if _, err := d.c.RecordPackageDeployment(ctx, pkgLayout.Pkg, deployedComponents, packageGeneration, state.WithPackageNamespaceOverride(opts.NamespaceOverride)); err != nil {
Expand Down Expand Up @@ -421,35 +433,35 @@ func (d *deployer) deployComponent(ctx context.Context, pkgLayout *layout.Packag
charts := []state.InstalledChart{}
if hasCharts {
helmCharts, err := d.installCharts(ctx, pkgLayout, component, opts)
charts = append(charts, helmCharts...)
if err != nil {
return nil, err
return charts, err
}
charts = append(charts, helmCharts...)
}

if hasManifests {
chartsFromManifests, err := d.installManifests(ctx, pkgLayout, component, opts)
charts = append(charts, chartsFromManifests...)
if err != nil {
return nil, err
return charts, err
}
charts = append(charts, chartsFromManifests...)
}

if err := actions.Run(ctx, cwd, onDeploy.Defaults, onDeploy.After, d.vc); err != nil {
return nil, fmt.Errorf("unable to run component after action: %w", err)
return charts, fmt.Errorf("unable to run component after action: %w", err)
}

if len(component.HealthChecks) > 0 {
healthCheckContext, cancel := context.WithTimeout(ctx, opts.Timeout)
defer cancel()
l.Info("running health checks")
if err := healthchecks.Run(healthCheckContext, d.c.Watcher, component.HealthChecks); err != nil {
return nil, fmt.Errorf("health checks failed: %w", err)
return charts, fmt.Errorf("health checks failed: %w", err)
}
}

if err := g.Wait(); err != nil {
return nil, err
return charts, err
}
l.Debug("done deploying component", "name", component.Name, "duration", time.Since(start))
return charts, nil
Expand Down Expand Up @@ -485,15 +497,15 @@ func (d *deployer) installCharts(ctx context.Context, pkgLayout *layout.PackageL
for idx := range chart.ValuesFiles {
valueFilePath := helm.StandardValuesName(valuesDir, chart, idx)
if err := d.vc.ReplaceTextTemplate(valueFilePath); err != nil {
return nil, err
return installedCharts, err
}
}

// Create a Helm values overrides map from set Zarf `variables` and DeployOpts library inputs
// Values overrides are to be applied in order of Helm Chart Defaults -> Zarf `valuesFiles` -> Zarf `variables` -> DeployOpts overrides
valuesOverrides, err := generateValuesOverrides(chart, component.Name, d.vc, opts.ValuesOverridesMap)
if err != nil {
return nil, err
return installedCharts, err
}

helmOpts := helm.InstallUpgradeOptions{
Expand All @@ -507,14 +519,15 @@ func (d *deployer) installCharts(ctx context.Context, pkgLayout *layout.PackageL
}
helmChart, values, err := helm.LoadChartData(chart, chartDir, valuesDir, valuesOverrides)
if err != nil {
return nil, fmt.Errorf("failed to load chart data: %w", err)
return installedCharts, fmt.Errorf("failed to load chart data: %w", err)
}

connectStrings, installedChartName, err := helm.InstallOrUpgradeChart(ctx, chart, helmChart, values, helmOpts)
if err != nil {
return nil, err
installedCharts = append(installedCharts, state.InstalledChart{Namespace: chart.Namespace, ChartName: installedChartName, ConnectStrings: connectStrings, Status: state.ChartStatusFailed})
return installedCharts, err
}
installedCharts = append(installedCharts, state.InstalledChart{Namespace: chart.Namespace, ChartName: installedChartName, ConnectStrings: connectStrings})
installedCharts = append(installedCharts, state.InstalledChart{Namespace: chart.Namespace, ChartName: installedChartName, ConnectStrings: connectStrings, Status: state.ChartStatusSucceeded})
}

return installedCharts, nil
Expand All @@ -540,7 +553,7 @@ func (d *deployer) installManifests(ctx context.Context, pkgLayout *layout.Packa
// The path is likely invalid because of how we compose OCI components, add an index suffix to the filename
manifest.Files[idx] = fmt.Sprintf("%s-%d.yaml", manifest.Name, idx)
if helpers.InvalidPath(filepath.Join(manifestDir, manifest.Files[idx])) {
return nil, fmt.Errorf("unable to find manifest file %s", manifest.Files[idx])
return installedCharts, fmt.Errorf("unable to find manifest file %s", manifest.Files[idx])
}
}
}
Expand All @@ -558,7 +571,7 @@ func (d *deployer) installManifests(ctx context.Context, pkgLayout *layout.Packa
// Create a helmChart and helm cfg from a given Zarf Manifest.
chart, helmChart, err := helm.ChartFromZarfManifest(manifest, manifestDir, pkgLayout.Pkg.Metadata.Name, component.Name)
if err != nil {
return nil, err
return installedCharts, err
}
helmOpts := helm.InstallUpgradeOptions{
AdoptExistingResources: opts.AdoptExistingResources,
Expand All @@ -573,9 +586,10 @@ func (d *deployer) installManifests(ctx context.Context, pkgLayout *layout.Packa
// Install the chart.
connectStrings, installedChartName, err := helm.InstallOrUpgradeChart(ctx, chart, helmChart, nil, helmOpts)
if err != nil {
return nil, err
installedCharts = append(installedCharts, state.InstalledChart{Namespace: manifest.Namespace, ChartName: installedChartName, ConnectStrings: connectStrings, Status: state.ChartStatusFailed})
return installedCharts, err
}
installedCharts = append(installedCharts, state.InstalledChart{Namespace: manifest.Namespace, ChartName: installedChartName, ConnectStrings: connectStrings})
installedCharts = append(installedCharts, state.InstalledChart{Namespace: manifest.Namespace, ChartName: installedChartName, ConnectStrings: connectStrings, Status: state.ChartStatusSucceeded})
}

return installedCharts, nil
Expand Down
55 changes: 55 additions & 0 deletions src/pkg/state/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ const (
ComponentStatusRemoving ComponentStatus = "Removing"
)

// All status options for a Zarf component chart
const (
ChartStatusSucceeded ChartStatus = "Succeeded"
ChartStatusFailed ChartStatus = "Failed"
)

// Values during setup of the initial zarf state
const (
ZarfGeneratedPasswordLen = 24
Expand Down Expand Up @@ -412,9 +418,58 @@ type DeployedComponent struct {
ObservedGeneration int `json:"observedGeneration"`
}

// ChartStatus is the status of a Helm Chart release
type ChartStatus string

// InstalledChart contains information about a Helm Chart that has been deployed to a cluster.
type InstalledChart struct {
Namespace string `json:"namespace"`
ChartName string `json:"chartName"`
ConnectStrings ConnectStrings `json:"connectStrings,omitempty"`
Status ChartStatus `json:"status"`
}

// MergeInstalledChartsForComponent merges the provided existing charts with the provided installed charts.
func MergeInstalledChartsForComponent(existingCharts, installedCharts []InstalledChart, partial bool) []InstalledChart {
key := func(chart InstalledChart) string {
return fmt.Sprintf("%s/%s", chart.Namespace, chart.ChartName)
}

lookup := make(map[string]InstalledChart, 0)
for _, chart := range existingCharts {
lookup[key(chart)] = chart
}

// Track which keys are still present in newCharts
seen := make(map[string]struct{}, len(installedCharts)+len(existingCharts))

for _, chart := range installedCharts {
k := key(chart)
seen[k] = struct{}{}

if _, ok := lookup[k]; ok {
existingChart := lookup[k]
existingChart.ConnectStrings = chart.ConnectStrings
existingChart.Status = chart.Status
lookup[k] = existingChart
} else {
lookup[k] = chart
}
}

// retain existing charts that are no longer present if not a partial
if !partial {
for k, chart := range lookup {
if _, ok := seen[k]; !ok {
lookup[k] = chart
}
}
}

merged := make([]InstalledChart, 0, len(lookup))
for _, chart := range lookup {
merged = append(merged, chart)
}

return merged
}
82 changes: 82 additions & 0 deletions src/pkg/state/state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,3 +283,85 @@ func TestMergeStateAgent(t *testing.T) {
require.NoError(t, err)
require.NotEqual(t, oldState.AgentTLS, newState.AgentTLS)
}

func TestMergeInstalledChartsForComponent(t *testing.T) {
t.Parallel()

tests := []struct {
name string
existingCharts []InstalledChart
installedCharts []InstalledChart
expectedCharts []InstalledChart
}{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit but I think partial should be a field on this table test as well

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair - partial has no functional purpose yet - but wanted to set us up for prune without having to make a breaking change.

lookup starts with the existing state and then adds each chart that was installed by the component. For prune - partial represents that we won't want to mark a release as untracked because deploy may have failed before it was deployed.

Open to removing it - but low overhead.

{
name: "existing charts are merged",
existingCharts: []InstalledChart{
{
Namespace: "default",
ChartName: "chart1",
},
{
Namespace: "default",
ChartName: "chart2",
},
},
installedCharts: []InstalledChart{
{
Namespace: "default",
ChartName: "chart3",
},
},
expectedCharts: []InstalledChart{
{
Namespace: "default",
ChartName: "chart1",
},
{
Namespace: "default",
ChartName: "chart2",
},
{
Namespace: "default",
ChartName: "chart3",
},
},
},
{
name: "overlapping charts are merged",
existingCharts: []InstalledChart{
{
Namespace: "default",
ChartName: "chart1",
},
{
Namespace: "default",
ChartName: "chart2",
},
},
installedCharts: []InstalledChart{
{
Namespace: "default",
ChartName: "chart1",
},
},
expectedCharts: []InstalledChart{
{
Namespace: "default",
ChartName: "chart1",
},
{
Namespace: "default",
ChartName: "chart2",
},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
actual := MergeInstalledChartsForComponent(tt.existingCharts, tt.installedCharts, false)
require.ElementsMatch(t, tt.expectedCharts, actual)
})
}
}
Loading
Loading