From 7f56d9beee48b4d1673efd13bc55507ab4c0c236 Mon Sep 17 00:00:00 2001 From: Wayne Starr Date: Thu, 25 Jun 2026 21:03:22 -0600 Subject: [PATCH 1/3] feat: add source and package status to package state Signed-off-by: Wayne Starr --- src/cmd/package.go | 13 ++++-- src/pkg/packager/deploy.go | 9 ++-- src/pkg/packager/remove.go | 47 ++++++++++++++++++--- src/pkg/state/state.go | 52 +++++++++++++++++++++--- src/test/e2e/37_component_status_test.go | 14 +++++-- src/test/e2e/41_remove_test.go | 19 +++++++-- 6 files changed, 129 insertions(+), 25 deletions(-) diff --git a/src/cmd/package.go b/src/cmd/package.go index d5eedebcef..65028ff132 100644 --- a/src/cmd/package.go +++ b/src/cmd/package.go @@ -383,6 +383,7 @@ func (o *packageDeployOptions) run(cmd *cobra.Command, args []string) (err error SetVariables: o.setVariables, NamespaceOverride: o.namespaceOverride, RemoteOptions: defaultRemoteOptions(), + Source: packageSource, IsInteractive: !o.confirm, SkipVersionCheck: o.skipVersionCheck, } @@ -1308,8 +1309,11 @@ func (o *packageListOptions) complete(ctx context.Context) error { // packageListInfo represents the package information for output. type packageListInfo struct { Package string `json:"package"` - NamespaceOverride string `json:"namespaceOverride"` + NamespaceOverride string `json:"namespaceOverride,omitempty"` Version string `json:"version"` + Digest string `json:"digest,omitempty"` + Source string `json:"source,omitempty"` + Status state.PackageStatus `json:"status"` Connectivity state.PackageConnectivity `json:"connectivity"` Components []string `json:"components"` } @@ -1330,6 +1334,9 @@ func (o *packageListOptions) run(ctx context.Context) error { Package: pkg.Name, NamespaceOverride: pkg.NamespaceOverride, Version: pkg.Data.Metadata.Version, + Digest: pkg.Digest, + Source: pkg.Source, + Status: pkg.GetStatus(), Connectivity: pkg.GetPackageConnectivity(), Components: components, }) @@ -1349,11 +1356,11 @@ func (o *packageListOptions) run(ctx context.Context) error { } fmt.Fprint(o.outputWriter, string(output)) case outputTable: - header := []string{"Package", "Namespace Override", "Version", "Connectivity", "Components"} + header := []string{"Package", "Namespace Override", "Version", "Status", "Connectivity", "Components"} var packageData [][]string for _, info := range packageList { packageData = append(packageData, []string{ - info.Package, info.NamespaceOverride, info.Version, string(info.Connectivity), fmt.Sprintf("%v", info.Components), + info.Package, info.NamespaceOverride, info.Version, string(info.Status), string(info.Connectivity), fmt.Sprintf("%v", info.Components), }) } message.TableWithWriter(o.outputWriter, header, packageData) diff --git a/src/pkg/packager/deploy.go b/src/pkg/packager/deploy.go index f67d1a132e..3d959aec3d 100644 --- a/src/pkg/packager/deploy.go +++ b/src/pkg/packager/deploy.go @@ -79,6 +79,9 @@ type DeployOptions struct { // [Library Only] A map of component names to chart names containing Helm Chart values to override values on deploy ValuesOverridesMap ValuesOverrides + // Source is the original source string used to load the package (e.g. oci:// URL or tarball path). + // Recorded in the cluster's deployed package state. + Source string // IsInteractive decides if Zarf can interactively prompt users through the CLI IsInteractive bool // SkipVersionCheck skips version requirement validation @@ -265,7 +268,7 @@ func (d *deployer) deployComponents(ctx context.Context, pkgLayout *layout.Packa deployedComponents = append(deployedComponents, deployedComponent) idx := len(deployedComponents) - 1 if d.isConnectedToCluster() { - if _, err := d.c.RecordPackageDeployment(ctx, pkgLayout.Pkg, pkgLayout.Digest(), deployedComponents, packageGeneration, state.WithPackageConnectivity(opts.Connected), state.WithPackageNamespaceOverride(opts.NamespaceOverride)); err != nil { + if _, err := d.c.RecordPackageDeployment(ctx, pkgLayout.Pkg, pkgLayout.Digest(), deployedComponents, packageGeneration, state.WithPackageConnectivity(opts.Connected), state.WithPackageNamespaceOverride(opts.NamespaceOverride), state.WithPackageSource(opts.Source), state.WithPackageStatus(state.PackageStatusDeploying)); err != nil { l.Debug("unable to record package deployment", "component", component.Name, "error", err.Error()) } } @@ -292,7 +295,7 @@ func (d *deployer) deployComponents(ctx context.Context, pkgLayout *layout.Packa 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, pkgLayout.Digest(), deployedComponents, packageGeneration, state.WithPackageConnectivity(opts.Connected), state.WithPackageNamespaceOverride(opts.NamespaceOverride)); err != nil { + if _, err := d.c.RecordPackageDeployment(ctx, pkgLayout.Pkg, pkgLayout.Digest(), deployedComponents, packageGeneration, state.WithPackageConnectivity(opts.Connected), state.WithPackageNamespaceOverride(opts.NamespaceOverride), state.WithPackageSource(opts.Source), state.WithPackageStatus(state.PackageStatusFailed)); err != nil { l.Debug("unable to record package deployment", "component", component.Name, "error", err.Error()) } } @@ -312,7 +315,7 @@ func (d *deployer) deployComponents(ctx context.Context, pkgLayout *layout.Packa 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, pkgLayout.Digest(), deployedComponents, packageGeneration, state.WithPackageConnectivity(opts.Connected), state.WithPackageNamespaceOverride(opts.NamespaceOverride)); err != nil { + if _, err := d.c.RecordPackageDeployment(ctx, pkgLayout.Pkg, pkgLayout.Digest(), deployedComponents, packageGeneration, state.WithPackageConnectivity(opts.Connected), state.WithPackageNamespaceOverride(opts.NamespaceOverride), state.WithPackageSource(opts.Source), state.WithPackageStatus(state.PackageStatusSucceeded)); err != nil { l.Debug("unable to record package deployment", "component", component.Name, "error", err.Error()) } } diff --git a/src/pkg/packager/remove.go b/src/pkg/packager/remove.go index 61e9c6ff4e..e40597d54b 100644 --- a/src/pkg/packager/remove.go +++ b/src/pkg/packager/remove.go @@ -91,6 +91,10 @@ func Remove(ctx context.Context, pkg v1alpha1.ZarfPackage, opts RemoveOptions) e if err != nil { return fmt.Errorf("unable to load the secret for the package we are attempting to remove: %w", err) } + depPkg.Status = state.PackageStatusRemoving + if err := opts.Cluster.UpdateDeployedPackage(ctx, *depPkg); err != nil { + l.Warn("unable to update package status to removing", "pkgName", depPkg.Name, "error", err.Error()) + } } else { // If we do not need the cluster, create a deployed components object based on the info we have depPkg.Name = pkg.Metadata.Name @@ -105,6 +109,20 @@ func Remove(ctx context.Context, pkg v1alpha1.ZarfPackage, opts RemoveOptions) e return fmt.Errorf("failed to get working directory: %w", err) } + // setCompStatus finds the component in depPkg.DeployedComponents by name and sets its status. + setCompStatus := func(name string, status state.ComponentStatus) { + if opts.Cluster == nil { + return + } + idx := slices.IndexFunc(depPkg.DeployedComponents, func(c state.DeployedComponent) bool { + return c.Name == name + }) + if idx < 0 { + return + } + depPkg.DeployedComponents[idx].Status = status + } + reverseDepComps := slices.Clone(depPkg.DeployedComponents) slices.Reverse(reverseDepComps) for _, depComp := range reverseDepComps { @@ -114,6 +132,11 @@ func Remove(ctx context.Context, pkg v1alpha1.ZarfPackage, opts RemoveOptions) e continue } + setCompStatus(depComp.Name, state.ComponentStatusRemoving) + if err := opts.Cluster.UpdateDeployedPackage(ctx, *depPkg); err != nil { + l.Warn("unable to update status", "component", depComp.Name, "status", state.ComponentStatusRemoving, "error", err.Error()) + } + err := func() error { err := actions.Run(ctx, cwd, comp.Actions.OnRemove.Defaults, comp.Actions.OnRemove.Before, nil, vals, template.StateAccess{}) if err != nil { @@ -168,6 +191,12 @@ func Remove(ctx context.Context, pkg v1alpha1.ZarfPackage, opts RemoveOptions) e return nil }() if err != nil { + depPkg.Status = state.PackageStatusRemoveFailed + setCompStatus(depComp.Name, state.ComponentStatusRemoveFailed) + if err := opts.Cluster.UpdateDeployedPackage(ctx, *depPkg); err != nil { + l.Warn("unable to update status", "component", depComp.Name, "status", state.ComponentStatusRemoveFailed, "error", err.Error()) + } + removeErr := actions.Run(ctx, cwd, comp.Actions.OnRemove.Defaults, comp.Actions.OnRemove.OnFailure, nil, vals, template.StateAccess{}) if removeErr != nil { return errors.Join(fmt.Errorf("unable to run the failure action: %w", err), removeErr) @@ -176,11 +205,19 @@ func Remove(ctx context.Context, pkg v1alpha1.ZarfPackage, opts RemoveOptions) e } } - // All the installed components were deleted, therefore this package is no longer actually deployed - if opts.Cluster != nil && len(depPkg.DeployedComponents) == 0 { - err := opts.Cluster.DeleteDeployedPackage(ctx, *depPkg) - if err != nil { - l.Warn("unable to delete secret for package, this may be normal if the cluster was removed", "pkgName", depPkg.Name, "error", err.Error()) + if opts.Cluster != nil { + if len(depPkg.DeployedComponents) == 0 { + // All installed components were deleted, therefore this package is no longer actually deployed + err := opts.Cluster.DeleteDeployedPackage(ctx, *depPkg) + if err != nil { + l.Warn("unable to delete secret for package, this may be normal if the cluster was removed", "pkgName", depPkg.Name, "error", err.Error()) + } + } else { + // Some components remain; reset the package status to Succeeded since the partial remove succeeded. + depPkg.Status = state.PackageStatusSucceeded + if err := opts.Cluster.UpdateDeployedPackage(ctx, *depPkg); err != nil { + l.Warn("unable to reset package status after partial remove", "pkgName", depPkg.Name, "error", err.Error()) + } } } diff --git a/src/pkg/state/state.go b/src/pkg/state/state.go index ede5d66f7d..c0049fbc70 100644 --- a/src/pkg/state/state.go +++ b/src/pkg/state/state.go @@ -87,15 +87,29 @@ func ParseServiceKey(s string) (ServiceKey, error) { return "", fmt.Errorf("invalid service key %q, valid keys are: %v", s, AllServiceKeys()) } +// PackageStatus defines the overall deployment status of a Zarf package. +type PackageStatus string + +// All the different status options for a Zarf Package +const ( + PackageStatusUnknown PackageStatus = "Unknown" + PackageStatusSucceeded PackageStatus = "Succeeded" + PackageStatusFailed PackageStatus = "Failed" + PackageStatusDeploying PackageStatus = "Deploying" + PackageStatusRemoving PackageStatus = "Removing" + PackageStatusRemoveFailed PackageStatus = "RemoveFailed" +) + // ComponentStatus defines the deployment status of a Zarf component within a package. type ComponentStatus string // All the different status options for a Zarf Component const ( - ComponentStatusSucceeded ComponentStatus = "Succeeded" - ComponentStatusFailed ComponentStatus = "Failed" - ComponentStatusDeploying ComponentStatus = "Deploying" - ComponentStatusRemoving ComponentStatus = "Removing" + ComponentStatusSucceeded ComponentStatus = "Succeeded" + ComponentStatusFailed ComponentStatus = "Failed" + ComponentStatusDeploying ComponentStatus = "Deploying" + ComponentStatusRemoving ComponentStatus = "Removing" + ComponentStatusRemoveFailed ComponentStatus = "RemoveFailed" ) // IPFamily defines the different possible IPfamilies that can be used in Kubernetes clusters @@ -627,6 +641,20 @@ func WithPackageConnectivity(connected bool) DeployedPackageOptions { } } +// WithPackageSource records the source string (e.g. oci:// URL or tarball path) used to deploy the package. +func WithPackageSource(source string) DeployedPackageOptions { + return func(o *DeployedPackage) { + o.Source = source + } +} + +// WithPackageStatus sets the overall deployment status of the package. +func WithPackageStatus(status PackageStatus) DeployedPackageOptions { + return func(o *DeployedPackage) { + o.Status = status + } +} + // PackageConnectivity defines the connectivity mode of package deployments type PackageConnectivity string @@ -640,8 +668,11 @@ const ( // DeployedPackage contains information about a Zarf Package that has been deployed to a cluster // This object is saved as the data of a k8s secret within the 'Zarf' namespace (not as part of the ZarfState secret). type DeployedPackage struct { - Name string `json:"name"` - Digest string `json:"digest"` + Name string `json:"name"` + Digest string `json:"digest"` + // Source is the original source string used to deploy the package (e.g. oci:// URL, path to tarball). + Source string `json:"source,omitempty"` + Status PackageStatus `json:"status,omitempty"` Data v1alpha1.ZarfPackage `json:"data"` CLIVersion string `json:"cliVersion"` Generation int `json:"generation"` @@ -674,6 +705,15 @@ func (d *DeployedPackage) GetPackageConnectivity() PackageConnectivity { return d.PackageConnectivity } +// GetStatus returns the overall deployment status of the package. +// Defaults to Unknown for packages deployed before status tracking was introduced. +func (d *DeployedPackage) GetStatus() PackageStatus { + if d.Status == "" { + return PackageStatusUnknown + } + return d.Status +} + // ConnectString contains information about a connection made with Zarf connect. type ConnectString struct { // Descriptive text that explains what the resource you would be connecting to is used for diff --git a/src/test/e2e/37_component_status_test.go b/src/test/e2e/37_component_status_test.go index 7ae7455820..d32a58cd91 100644 --- a/src/test/e2e/37_component_status_test.go +++ b/src/test/e2e/37_component_status_test.go @@ -59,9 +59,13 @@ func TestComponentStatus(t *testing.T) { errCh <- fmt.Errorf("expected 1 component got %d", len(deployedPackage.DeployedComponents)) return } - status := deployedPackage.DeployedComponents[0].Status - if status != state.ComponentStatusDeploying { - errCh <- fmt.Errorf("expected %s got %s", state.ComponentStatusDeploying, status) + compStatus := deployedPackage.DeployedComponents[0].Status + if compStatus != state.ComponentStatusDeploying { + errCh <- fmt.Errorf("expected component status %s got %s", state.ComponentStatusDeploying, compStatus) + return + } + if deployedPackage.Status != state.PackageStatusDeploying { + errCh <- fmt.Errorf("expected package status %s got %s", state.PackageStatusDeploying, deployedPackage.Status) return } time.Sleep(2 * time.Second) @@ -78,13 +82,15 @@ func TestComponentStatus(t *testing.T) { require.NoError(t, err) default: } - // Verify that the component status is "succeeded" + // Verify that the component and package statuses are "succeeded" and source is recorded stdOut, stdErr, err = e2e.Kubectl(t, "get", "secret", "zarf-package-component-status", "-n", "zarf", "-o", "jsonpath={.data.data}") require.NoError(t, err, stdOut, stdErr) deployedPackage, err := getDeployedPackage(stdOut) require.NoError(t, err) require.Len(t, deployedPackage.DeployedComponents, 1) require.Equal(t, state.ComponentStatusSucceeded, deployedPackage.DeployedComponents[0].Status) + require.Equal(t, state.PackageStatusSucceeded, deployedPackage.Status) + require.Equal(t, path, deployedPackage.Source) // Remove the package t.Cleanup(func() { stdOut, stdErr, err = e2e.Zarf(t, "package", "remove", "component-status", "--confirm") diff --git a/src/test/e2e/41_remove_test.go b/src/test/e2e/41_remove_test.go index b60cc57835..ab6dcb1158 100644 --- a/src/test/e2e/41_remove_test.go +++ b/src/test/e2e/41_remove_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "github.com/zarf-dev/zarf/src/pkg/cluster" + "github.com/zarf-dev/zarf/src/pkg/state" ) func TestRemovePackageComponents(t *testing.T) { @@ -29,6 +30,14 @@ func TestRemovePackageComponents(t *testing.T) { stdOut, stdErr, err = e2e.Zarf(t, "package", "deploy", packagePath, "--confirm") require.NoError(t, err, stdOut, stdErr) + // Verify package status is Succeeded and source is recorded after successful deploy + c, err := cluster.New(t.Context()) + require.NoError(t, err) + deployedPackage, err := c.GetDeployedPackage(t.Context(), "remove-test") + require.NoError(t, err) + require.Equal(t, state.PackageStatusSucceeded, deployedPackage.Status) + require.Equal(t, packagePath, deployedPackage.Source) + // Asking for removal of a component that doesn't exist should error stdOut, stdErr, err = e2e.Zarf(t, "package", "remove", "remove-test", "--components=unknown_component", "--confirm") require.Error(t, err, stdOut, stdErr) @@ -37,12 +46,11 @@ func TestRemovePackageComponents(t *testing.T) { stdOut, stdErr, err = e2e.Zarf(t, "package", "remove", "remove-test", "--components=first", "--confirm") require.NoError(t, err, stdOut, stdErr) - c, err := cluster.New(t.Context()) - require.NoError(t, err) - deployedPackage, err := c.GetDeployedPackage(t.Context(), "remove-test") + deployedPackage, err = c.GetDeployedPackage(t.Context(), "remove-test") require.NoError(t, err) require.Len(t, deployedPackage.DeployedComponents, 1) require.Equal(t, "second", deployedPackage.DeployedComponents[0].Name) + require.Equal(t, state.PackageStatusSucceeded, deployedPackage.Status) stdOut, stdErr, err = e2e.Zarf(t, "package", "remove", packagePath, "--components=second", "--confirm") require.NoError(t, err, stdOut, stdErr) @@ -68,13 +76,16 @@ func TestRemoveFailedPackagedComponents(t *testing.T) { stdOut, stdErr, err = e2e.Zarf(t, "package", "deploy", packagePath, "--confirm", "--timeout", "3s") require.Error(t, err, stdOut, stdErr) - // check state that the installedChart is deployed and recorded in state + // check state that the installedChart is deployed and recorded in state, and that status reflects failure c, err := cluster.New(t.Context()) require.NoError(t, err) deployedPackage, err := c.GetDeployedPackage(t.Context(), "failing-deploy-remove-test") require.NoError(t, err) + require.Equal(t, state.PackageStatusFailed, deployedPackage.Status) + require.Equal(t, packagePath, deployedPackage.Source) require.Len(t, deployedPackage.DeployedComponents, 2) require.Equal(t, "second", deployedPackage.DeployedComponents[1].Name) + require.Equal(t, state.ComponentStatusFailed, deployedPackage.DeployedComponents[1].Status) require.Len(t, deployedPackage.DeployedComponents[1].InstalledCharts, 1) // remove the package by component From 657ec3915a6ad6ad539a7b5e3c34be656bb9df17 Mon Sep 17 00:00:00 2001 From: Wayne Starr Date: Thu, 25 Jun 2026 21:28:47 -0600 Subject: [PATCH 2/3] update tests Signed-off-by: Wayne Starr --- src/cmd/testdata/package-list/expected.json | 3 ++- src/cmd/testdata/package-list/expected.yaml | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/cmd/testdata/package-list/expected.json b/src/cmd/testdata/package-list/expected.json index 683b0ea214..f5e6e28c4b 100644 --- a/src/cmd/testdata/package-list/expected.json +++ b/src/cmd/testdata/package-list/expected.json @@ -1,8 +1,8 @@ [ { "package": "package1", - "namespaceOverride": "", "version": "0.42.0", + "status": "Unknown", "connectivity": "airgap", "components": [ "component1", @@ -13,6 +13,7 @@ "package": "package2", "namespaceOverride": "test2", "version": "1.0.0", + "status": "Unknown", "connectivity": "airgap", "components": [ "component3", diff --git a/src/cmd/testdata/package-list/expected.yaml b/src/cmd/testdata/package-list/expected.yaml index e4d60c281a..1cb6c9969b 100644 --- a/src/cmd/testdata/package-list/expected.yaml +++ b/src/cmd/testdata/package-list/expected.yaml @@ -1,13 +1,14 @@ - package: package1 - namespaceOverride: "" version: 0.42.0 + status: Unknown connectivity: airgap components: - component1 - component2 - package: package2 - namespaceOverride: "test2" + namespaceOverride: test2 version: 1.0.0 + status: Unknown connectivity: airgap components: - component3 From a1c7dcfd5ccde67ad3326a06fdd7cb4f9f297620 Mon Sep 17 00:00:00 2001 From: Wayne Starr Date: Fri, 26 Jun 2026 15:03:49 -0600 Subject: [PATCH 3/3] fix nil cluster Signed-off-by: Wayne Starr --- src/pkg/packager/remove.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/pkg/packager/remove.go b/src/pkg/packager/remove.go index e40597d54b..8a5c9b0284 100644 --- a/src/pkg/packager/remove.go +++ b/src/pkg/packager/remove.go @@ -133,8 +133,10 @@ func Remove(ctx context.Context, pkg v1alpha1.ZarfPackage, opts RemoveOptions) e } setCompStatus(depComp.Name, state.ComponentStatusRemoving) - if err := opts.Cluster.UpdateDeployedPackage(ctx, *depPkg); err != nil { - l.Warn("unable to update status", "component", depComp.Name, "status", state.ComponentStatusRemoving, "error", err.Error()) + if opts.Cluster != nil { + if err := opts.Cluster.UpdateDeployedPackage(ctx, *depPkg); err != nil { + l.Warn("unable to update status", "component", depComp.Name, "status", state.ComponentStatusRemoving, "error", err.Error()) + } } err := func() error { @@ -193,8 +195,10 @@ func Remove(ctx context.Context, pkg v1alpha1.ZarfPackage, opts RemoveOptions) e if err != nil { depPkg.Status = state.PackageStatusRemoveFailed setCompStatus(depComp.Name, state.ComponentStatusRemoveFailed) - if err := opts.Cluster.UpdateDeployedPackage(ctx, *depPkg); err != nil { - l.Warn("unable to update status", "component", depComp.Name, "status", state.ComponentStatusRemoveFailed, "error", err.Error()) + if opts.Cluster != nil { + if err := opts.Cluster.UpdateDeployedPackage(ctx, *depPkg); err != nil { + l.Warn("unable to update status", "component", depComp.Name, "status", state.ComponentStatusRemoveFailed, "error", err.Error()) + } } removeErr := actions.Run(ctx, cwd, comp.Actions.OnRemove.Defaults, comp.Actions.OnRemove.OnFailure, nil, vals, template.StateAccess{})