Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions src/cmd/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,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,
}
Expand Down Expand Up @@ -1326,8 +1327,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"`
}
Expand All @@ -1348,6 +1352,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,
})
Expand All @@ -1367,11 +1374,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)
Expand Down
3 changes: 2 additions & 1 deletion src/cmd/testdata/package-list/expected.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
[
{
"package": "package1",
"namespaceOverride": "",
"version": "0.42.0",
"status": "Unknown",
"connectivity": "airgap",
"components": [
"component1",
Expand All @@ -13,6 +13,7 @@
"package": "package2",
"namespaceOverride": "test2",
"version": "1.0.0",
"status": "Unknown",
"connectivity": "airgap",
"components": [
"component3",
Expand Down
5 changes: 3 additions & 2 deletions src/cmd/testdata/package-list/expected.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
9 changes: 6 additions & 3 deletions src/pkg/packager/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

From a consumer perspective, will you care about the actual source or only which type of source. If the latter, then an enum here might be better

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Actual source - tarballs are hard to reconstruct but an oci:// reference for example could be repulled and deployed again if you needed to.

// IsInteractive decides if Zarf can interactively prompt users through the CLI
IsInteractive bool
// SkipVersionCheck skips version requirement validation
Expand Down Expand Up @@ -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())
}
}
Expand All @@ -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())
}
}
Expand All @@ -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())
}
}
Expand Down
51 changes: 46 additions & 5 deletions src/pkg/packager/remove.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -114,6 +132,13 @@ func Remove(ctx context.Context, pkg v1alpha1.ZarfPackage, opts RemoveOptions) e
continue
}

setCompStatus(depComp.Name, state.ComponentStatusRemoving)
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 {
err := actions.Run(ctx, cwd, comp.Actions.OnRemove.Defaults, comp.Actions.OnRemove.Before, nil, vals, template.StateAccess{})
if err != nil {
Expand Down Expand Up @@ -168,6 +193,14 @@ 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 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{})
if removeErr != nil {
return errors.Join(fmt.Errorf("unable to run the failure action: %w", err), removeErr)
Expand All @@ -176,11 +209,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())
}
}
}

Expand Down
52 changes: 46 additions & 6 deletions src/pkg/state/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm curious what you'd think of splitting out the state of the lifecycle with the health of the package. Something like below, this could also apply to components, though we'd want to keep their current status for some time for backward compatibility

// PackagePhase is the current lifecycle operation.
type PackagePhase string

const (
      PackagePhaseIdle      PackagePhase = "Idle" 
      PackagePhaseDeploying PackagePhase = "Deploying"
      PackagePhaseRemoving  PackagePhase = "Removing"
)

// PackageHealth is the outcome of the most recent completed operation.
type PackageOutcome string

const (
      PackageOutcomeUnknown  PackageOutcome = "Unknown" 
      PackageOutcomeHealthy    PackageOutcome = "Healthy" 
      PackageOutcomeDegraded PackageOutcome = "Degraded"
)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

added this in zarf-dev/proposals#27


// 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
Expand Down Expand Up @@ -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

Expand All @@ -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"`
Expand Down Expand Up @@ -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
Expand Down
14 changes: 10 additions & 4 deletions src/test/e2e/37_component_status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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")
Expand Down
19 changes: 15 additions & 4 deletions src/test/e2e/41_remove_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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
Expand Down
Loading