Skip to content

feat: recording deployed component status in package deploy #3556

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Mar 10, 2025
Merged
2 changes: 1 addition & 1 deletion src/internal/packager2/load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ func TestPackageFromSourceOrCluster(t *testing.T) {
c := &cluster.Cluster{
Clientset: fake.NewClientset(),
}
_, err = c.RecordPackageDeployment(ctx, pkg, nil)
_, err = c.RecordPackageDeployment(ctx, pkg, nil, 1)
require.NoError(t, err)
pkg, err = GetPackageFromSourceOrCluster(ctx, c, "test", false, "")
require.NoError(t, err)
Expand Down
3 changes: 2 additions & 1 deletion src/pkg/cluster/zarf.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ func (c *Cluster) StripZarfLabelsAndSecretsFromNamespaces(ctx context.Context) {
}

// RecordPackageDeployment saves metadata about a package that has been deployed to the cluster.
func (c *Cluster) RecordPackageDeployment(ctx context.Context, pkg v1alpha1.ZarfPackage, components []types.DeployedComponent) (*types.DeployedPackage, error) {
func (c *Cluster) RecordPackageDeployment(ctx context.Context, pkg v1alpha1.ZarfPackage, components []types.DeployedComponent, generation int) (*types.DeployedPackage, error) {
packageName := pkg.Metadata.Name

// TODO: This is done for backwards compatibility and could be removed in the future.
Expand All @@ -175,6 +175,7 @@ func (c *Cluster) RecordPackageDeployment(ctx context.Context, pkg v1alpha1.Zarf
Data: pkg,
DeployedComponents: components,
ConnectStrings: connectStrings,
Generation: generation,
}

packageData, err := json.Marshal(deployedPackage)
Expand Down
23 changes: 18 additions & 5 deletions src/pkg/packager/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ func (p *Packager) deployComponents(ctx context.Context) ([]types.DeployedCompon

// Process all the components we are deploying
for _, component := range p.cfg.Pkg.Components {
packageGeneration := 1
// Connect to cluster if a component requires it.
if component.RequiresCluster() {
timeout := cluster.DefaultTimeout
Expand All @@ -162,10 +163,16 @@ func (p *Packager) deployComponents(ctx context.Context) ([]types.DeployedCompon
if err := p.connectToCluster(connectCtx); err != nil {
return nil, fmt.Errorf("unable to connect to the Kubernetes cluster: %w", err)
}
// If this package has been deployed before, increment the package generation within the secret
if existingDeployedPackage, _ := p.cluster.GetDeployedPackage(ctx, p.cfg.Pkg.Metadata.Name); existingDeployedPackage != nil {
packageGeneration = existingDeployedPackage.Generation + 1
}
}

deployedComponent := types.DeployedComponent{
Name: component.Name,
Name: component.Name,
Status: types.ComponentStatusDeploying,
ObservedGeneration: packageGeneration,
}

// Ensure we don't overwrite any installedCharts data when updating the package secret
Expand All @@ -180,7 +187,12 @@ func (p *Packager) deployComponents(ctx context.Context) ([]types.DeployedCompon

deployedComponents = append(deployedComponents, deployedComponent)
idx := len(deployedComponents) - 1

if p.isConnectedToCluster() {
if _, err := p.cluster.RecordPackageDeployment(ctx, p.cfg.Pkg, deployedComponents, packageGeneration); err != nil {
message.Debugf("Unable to record package deployment for component %q: this will affect features like `zarf package remove`: %s", component.Name, err.Error())
l.Debug("unable to record package deployment", "component", component.Name, "error", err.Error())
}
}
// Deploy the component
var charts []types.InstalledChart
var deployErr error
Expand All @@ -201,9 +213,9 @@ func (p *Packager) deployComponents(ctx context.Context) ([]types.DeployedCompon

if deployErr != nil {
onFailure()

deployedComponents[idx].Status = types.ComponentStatusFailed
if p.isConnectedToCluster() {
if _, err := p.cluster.RecordPackageDeployment(ctx, p.cfg.Pkg, deployedComponents); err != nil {
if _, err := p.cluster.RecordPackageDeployment(ctx, p.cfg.Pkg, deployedComponents, packageGeneration); err != nil {
message.Debugf("Unable to record package deployment for component %q: this will affect features like `zarf package remove`: %s", component.Name, err.Error())
l.Debug("unable to record package deployment", "component", component.Name, "error", err.Error())
}
Expand All @@ -213,8 +225,9 @@ func (p *Packager) deployComponents(ctx context.Context) ([]types.DeployedCompon

// Update the package secret to indicate that we successfully deployed this component
deployedComponents[idx].InstalledCharts = charts
deployedComponents[idx].Status = types.ComponentStatusSucceeded
if p.isConnectedToCluster() {
if _, err := p.cluster.RecordPackageDeployment(ctx, p.cfg.Pkg, deployedComponents); err != nil {
if _, err := p.cluster.RecordPackageDeployment(ctx, p.cfg.Pkg, deployedComponents, packageGeneration); err != nil {
message.Debugf("Unable to record package deployment for component %q: this will affect features like `zarf package remove`: %s", component.Name, err.Error())
l.Debug("unable to record package deployment", "component", component.Name, "error", err.Error())
}
Expand Down
106 changes: 106 additions & 0 deletions src/test/e2e/37_component_status_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2021-Present The Zarf Authors

// Package test provides e2e tests for Zarf.
package test

import (
"encoding/base64"
"encoding/json"
"fmt"
"path/filepath"
"sync"
"testing"
"time"

"github.com/stretchr/testify/require"
"github.com/zarf-dev/zarf/src/types"
)

func TestComponentStatus(t *testing.T) {
t.Log("E2E: Component Status")
tmpDir := t.TempDir()
_, _, err := e2e.Zarf(t, "package", "create", "src/test/packages/37-component-status", "-o", tmpDir, "--confirm")
require.NoError(t, err)
packageName := fmt.Sprintf("zarf-package-component-status-%s.tar.zst", e2e.Arch)
path := filepath.Join(tmpDir, packageName)
// Stop channel getting the zarf state
stop := make(chan bool)
// Error channel to return any errors from the goroutine. Testify doesn't like require in a goroutine
errCh := make(chan error, 1)
var wg sync.WaitGroup
wg.Add(1)
// Goroutine to wait for the package to show "deploying" status
go func() {
defer wg.Done()
// Give extra time to build and push the package
ticker := time.NewTicker(30 * time.Second)
for {
select {
case <-ticker.C:
t.Log("Timed out waiting for package to deploy")
errCh <- fmt.Errorf("timed out waiting for package to deploy")
return
case <-stop:
return
default:
stdOut, _, err := e2e.Kubectl(t, "get", "secret", "zarf-package-component-status", "-n", "zarf", "-o", "jsonpath={.data.data}")
if err != nil {
// Wait for the secret to be created and try again
time.Sleep(2 * time.Second)
continue
}
deployedPackage, err := getDeployedPackage(stdOut)
if err != nil {
errCh <- err
return
}
if len(deployedPackage.DeployedComponents) != 1 {
errCh <- fmt.Errorf("expected 1 component got %d", len(deployedPackage.DeployedComponents))
return
}
status := deployedPackage.DeployedComponents[0].Status
if status != types.ComponentStatusDeploying {
errCh <- fmt.Errorf("expected %s got %s", types.ComponentStatusDeploying, status)
return
}
time.Sleep(2 * time.Second)
return
}
}
}()
stdOut, stdErr, err := e2e.Zarf(t, "package", "deploy", path, "--confirm")
require.NoError(t, err, stdOut, stdErr)
close(stop)
wg.Wait()
select {
case err := <-errCh:
require.NoError(t, err)
default:
}
// Verify that the component status is "succeeded"
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, types.ComponentStatusSucceeded, deployedPackage.DeployedComponents[0].Status)
// Remove the package
t.Cleanup(func() {
stdOut, stdErr, err = e2e.Zarf(t, "package", "remove", "component-status", "--confirm")
require.NoError(t, err, stdOut, stdErr)
})
}

func getDeployedPackage(secret string) (*types.DeployedPackage, error) {
deployedPackage := &types.DeployedPackage{}
decoded, err := base64.StdEncoding.DecodeString(secret)
if err != nil {
return nil, err
}
err = json.Unmarshal(decoded, &deployedPackage)
if err != nil {
return nil, err
}
return deployedPackage, nil
}
6 changes: 3 additions & 3 deletions src/test/packages/22-git-data/zarf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ components:
onDeploy:
before:
# Check to verify the package secret has been saved for the already deployed component
- cmd: test $(./zarf tools kubectl get secret -n zarf zarf-package-git-data-test -o jsonpath='{.data.*}' | base64 --decode | jq -r .deployedComponents | jq '. | length') -eq 1
- cmd: test $(./zarf tools kubectl get secret -n zarf zarf-package-git-data-test -o jsonpath='{.data.*}' | base64 --decode | jq -r .deployedComponents | jq '. | length') -eq 2
description: Check that the package secret has been updated with the deployed component
maxRetries: 3

Expand All @@ -41,7 +41,7 @@ components:
onDeploy:
before:
# Check to verify the package secret has been saved for the already deployed component
- cmd: test $(./zarf tools kubectl get secret -n zarf zarf-package-git-data-test -o jsonpath='{.data.*}' | base64 --decode | jq -r .deployedComponents | jq '. | length') -eq 2
- cmd: test $(./zarf tools kubectl get secret -n zarf zarf-package-git-data-test -o jsonpath='{.data.*}' | base64 --decode | jq -r .deployedComponents | jq '. | length') -eq 3
description: Check that the package secret has been updated with the deployed component
maxRetries: 3

Expand All @@ -56,7 +56,7 @@ components:
onDeploy:
before:
# Check to verify the package secret has been saved for the already deployed component
- cmd: test $(./zarf tools kubectl get secret -n zarf zarf-package-git-data-test -o jsonpath='{.data.*}' | base64 --decode | jq -r .deployedComponents | jq '. | length') -eq 3
- cmd: test $(./zarf tools kubectl get secret -n zarf zarf-package-git-data-test -o jsonpath='{.data.*}' | base64 --decode | jq -r .deployedComponents | jq '. | length') -eq 4
description: Check that the package secret has been updated with the deployed component
maxRetries: 3
onSuccess:
Expand Down
13 changes: 13 additions & 0 deletions src/test/packages/37-component-status/component-status.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
apiVersion: v1
kind: Pod
metadata:
name: component-status
spec:
# Extra security to ensure the pod isn't ready before the health checks run
initContainers:
- name: init-wait
image: ghcr.io/stefanprodan/podinfo:6.4.0
command: ["sh", "-c", "sleep 10"]
containers:
- name: component-status
image: ghcr.io/stefanprodan/podinfo:6.4.0
15 changes: 15 additions & 0 deletions src/test/packages/37-component-status/zarf.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
kind: ZarfPackageConfig
metadata:
name: component-status
description: Test the status of components

components:
- name: component-status
required: true
manifests:
- name: first-component
namespace: first-component
files:
- component-status.yaml
images:
- ghcr.io/stefanprodan/podinfo:6.4.0
7 changes: 5 additions & 2 deletions src/types/k8s.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ type DeployedPackage struct {
Name string `json:"name"`
Data v1alpha1.ZarfPackage `json:"data"`
CLIVersion string `json:"cliVersion"`
Generation int `json:"generation"`
DeployedComponents []DeployedComponent `json:"deployedComponents"`
ConnectStrings ConnectStrings `json:"connectStrings,omitempty"`
}
Expand All @@ -89,8 +90,10 @@ type ConnectStrings map[string]ConnectString

// DeployedComponent contains information about a Zarf Package Component that has been deployed to a cluster.
type DeployedComponent struct {
Name string `json:"name"`
InstalledCharts []InstalledChart `json:"installedCharts"`
Name string `json:"name"`
InstalledCharts []InstalledChart `json:"installedCharts"`
Status ComponentStatus `json:"status"`
ObservedGeneration int `json:"observedGeneration"`
}

// InstalledChart contains information about a Helm Chart that has been deployed to a cluster.
Expand Down
Loading