From 1926a21e57d32a2436aaccd5115529defb3a76ff Mon Sep 17 00:00:00 2001 From: razzle Date: Thu, 26 Oct 2023 15:14:54 -0500 Subject: [PATCH] fix: improve behavior around cluster connection during deploy (#2088) ## Description Fixes #2085 ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Other (security config, docs update, etc) ## Checklist before merging - [ ] Test, docs, adr added or updated as needed - [x] [Contributor Guide Steps](https://github.com/defenseunicorns/zarf/blob/main/CONTRIBUTING.md#developer-workflow) followed --------- Signed-off-by: razzle Co-authored-by: Wayne Starr Co-authored-by: Wayne Starr --- src/config/lang/english.go | 4 +- src/internal/cluster/state.go | 17 +-- src/pkg/k8s/info.go | 22 +++- src/pkg/packager/common.go | 85 +++++++++++++-- src/pkg/packager/common_test.go | 57 +++++++--- src/pkg/packager/components.go | 15 +-- src/pkg/packager/create.go | 2 +- src/pkg/packager/deploy.go | 100 ++++++------------ src/pkg/packager/deprecated/common.go | 4 +- src/pkg/packager/mirror.go | 8 -- src/pkg/packager/publish.go | 2 +- src/pkg/packager/remove.go | 8 +- src/pkg/packager/sources/tarball.go | 5 +- src/pkg/utils/helpers/misc.go | 10 ++ .../e2e/29_mismatched_architectures_test.go | 35 ------ src/test/e2e/29_mismatched_checks_test.go | 90 ++++++++++++++++ 16 files changed, 291 insertions(+), 173 deletions(-) delete mode 100644 src/test/e2e/29_mismatched_architectures_test.go create mode 100644 src/test/e2e/29_mismatched_checks_test.go diff --git a/src/config/lang/english.go b/src/config/lang/english.go index 75c7a13117..c185be79d7 100644 --- a/src/config/lang/english.go +++ b/src/config/lang/english.go @@ -259,8 +259,8 @@ const ( CmdPackageDeployFlagShasum = "Shasum of the package to deploy. Required if deploying a remote package and \"--insecure\" is not provided" CmdPackageDeployFlagSget = "[Deprecated] Path to public sget key file for remote packages signed via cosign. This flag will be removed in v1.0.0 please use the --key flag instead." CmdPackageDeployFlagSkipWebhooks = "[alpha] Skip waiting for external webhooks to execute as each package component is deployed" - CmdPackageDeployValidateArchitectureErr = "this package architecture is %s, but the target cluster has the %s architecture. These architectures must be the same" - CmdPackageDeployValidateLastNonBreakingVersionWarn = "the version of this Zarf binary '%s' is less than the LastNonBreakingVersion of '%s'. You may need to upgrade your Zarf version to at least '%s' to deploy this package" + CmdPackageDeployValidateArchitectureErr = "this package architecture is %s, but the target cluster only has the %s architecture(s). These architectures must be compatible when \"images\" are present" + CmdPackageDeployValidateLastNonBreakingVersionWarn = "The version of this Zarf binary '%s' is less than the LastNonBreakingVersion of '%s'. You may need to upgrade your Zarf version to at least '%s' to deploy this package" CmdPackageDeployInvalidCLIVersionWarn = "CLIVersion is set to '%s' which can cause issues with package creation and deployment. To avoid such issues, please set the value to the valid semantic version for this version of Zarf." CmdPackageDeployErr = "Failed to deploy package: %s" diff --git a/src/internal/cluster/state.go b/src/internal/cluster/state.go index ba9888c26f..af13f4322f 100644 --- a/src/internal/cluster/state.go +++ b/src/internal/cluster/state.go @@ -35,19 +35,13 @@ const ( // InitZarfState initializes the Zarf state with the given temporary directory and init configs. func (c *Cluster) InitZarfState(initOptions types.ZarfInitOptions) error { var ( - clusterArch string - distro string - err error + distro string + err error ) - spinner := message.NewProgressSpinner("Gathering cluster information") + spinner := message.NewProgressSpinner("Gathering cluster state information") defer spinner.Stop() - spinner.Updatef("Getting cluster architecture") - if clusterArch, err = c.GetArchitecture(); err != nil { - spinner.Errorf(err, "Unable to validate the cluster system architecture") - } - // Attempt to load an existing state prior to init. // NOTE: We are ignoring the error here because we don't really expect a state to exist yet. spinner.Updatef("Checking cluster for existing Zarf deployment") @@ -77,7 +71,6 @@ func (c *Cluster) InitZarfState(initOptions types.ZarfInitOptions) error { // Defaults state.Distro = distro - state.Architecture = clusterArch state.LoggingSecret = utils.RandomString(config.ZarfGeneratedPasswordLen) // Setup zarf agent PKI @@ -134,10 +127,6 @@ func (c *Cluster) InitZarfState(initOptions types.ZarfInitOptions) error { } } - if clusterArch != state.Architecture { - return fmt.Errorf("cluster architecture %s does not match the Zarf state architecture %s", clusterArch, state.Architecture) - } - switch state.Distro { case k8s.DistroIsK3s, k8s.DistroIsK3d: state.StorageClass = "local-path" diff --git a/src/pkg/k8s/info.go b/src/pkg/k8s/info.go index 3dabd9363a..61effabdaa 100644 --- a/src/pkg/k8s/info.go +++ b/src/pkg/k8s/info.go @@ -114,18 +114,30 @@ func (k *K8s) DetectDistro() (string, error) { return DistroIsUnknown, nil } -// GetArchitecture returns the cluster system architecture if found or an error if not. -func (k *K8s) GetArchitecture() (string, error) { +// GetArchitectures returns the cluster system architectures if found. +func (k *K8s) GetArchitectures() ([]string, error) { nodes, err := k.GetNodes() if err != nil { - return "", err + return nil, err } + if len(nodes.Items) == 0 { + return nil, errors.New("could not identify node architecture") + } + + archMap := map[string]bool{} + for _, node := range nodes.Items { - return node.Status.NodeInfo.Architecture, nil + archMap[node.Status.NodeInfo.Architecture] = true + } + + architectures := []string{} + + for arch := range archMap { + architectures = append(architectures, arch) } - return "", errors.New("could not identify node architecture") + return architectures, nil } // GetServerVersion retrieves and returns the k8s revision. diff --git a/src/pkg/packager/common.go b/src/pkg/packager/common.go index 2adb24cc73..6d4d3606e4 100644 --- a/src/pkg/packager/common.go +++ b/src/pkg/packager/common.go @@ -6,11 +6,13 @@ package packager import ( "encoding/json" + "errors" "fmt" "os" "path/filepath" "regexp" "strings" + "time" "github.com/Masterminds/semver/v3" "github.com/defenseunicorns/zarf/src/config/lang" @@ -19,12 +21,14 @@ import ( "github.com/defenseunicorns/zarf/src/internal/packager/template" "github.com/defenseunicorns/zarf/src/types" "github.com/mholt/archiver/v3" + "k8s.io/utils/strings/slices" "github.com/defenseunicorns/zarf/src/config" "github.com/defenseunicorns/zarf/src/pkg/interactive" "github.com/defenseunicorns/zarf/src/pkg/layout" "github.com/defenseunicorns/zarf/src/pkg/message" "github.com/defenseunicorns/zarf/src/pkg/oci" + "github.com/defenseunicorns/zarf/src/pkg/packager/deprecated" "github.com/defenseunicorns/zarf/src/pkg/packager/sources" "github.com/defenseunicorns/zarf/src/pkg/utils" ) @@ -174,7 +178,7 @@ func GetInitPackageName(arch string) string { // GetPackageName returns the formatted name of the package. func (p *Packager) GetPackageName() string { - if p.cfg.Pkg.Kind == types.ZarfInitConfig { + if p.isInitConfig() { return GetInitPackageName(p.arch) } @@ -201,21 +205,90 @@ func (p *Packager) ClearTempPaths() { _ = os.RemoveAll(layout.SBOMDir) } +// connectToCluster attempts to connect to a cluster if a connection is not already established +func (p *Packager) connectToCluster(timeout time.Duration) (err error) { + if p.isConnectedToCluster() { + return nil + } + + p.cluster, err = cluster.NewClusterWithWait(timeout) + if err != nil { + return err + } + + return p.attemptClusterChecks() +} + +// isConnectedToCluster returns whether the current packager instance is connected to a cluster +func (p *Packager) isConnectedToCluster() bool { + return p.cluster != nil +} + +// isInitConfig returns whether the current packager instance is deploying an init config +func (p *Packager) isInitConfig() bool { + return p.cfg.Pkg.Kind == types.ZarfInitConfig +} + +// hasImages returns whether the current package contains images +func (p *Packager) hasImages() bool { + for _, component := range p.cfg.Pkg.Components { + if len(component.Images) > 0 { + return true + } + } + return false +} + +// attemptClusterChecks attempts to connect to the cluster and check for useful metadata and config mismatches. +// NOTE: attemptClusterChecks should only return an error if there is a problem significant enough to halt a deployment, otherwise it should return nil and print a warning message. +func (p *Packager) attemptClusterChecks() (err error) { + + spinner := message.NewProgressSpinner("Gathering additional cluster information (if available)") + defer spinner.Stop() + + // Check if the package has already been deployed and get its generation + if existingDeployedPackage, _ := p.cluster.GetDeployedPackage(p.cfg.Pkg.Metadata.Name); existingDeployedPackage != nil { + // If this package has been deployed before, increment the package generation within the secret + p.generation = existingDeployedPackage.Generation + 1 + } + + // Check the clusters architecture matches the package spec + if err := p.validatePackageArchitecture(); err != nil { + if errors.Is(err, lang.ErrUnableToCheckArch) { + message.Warnf("Unable to validate package architecture: %s", err.Error()) + } else { + return err + } + } + + // Check for any breaking changes between the initialized Zarf version and this CLI + if existingInitPackage, _ := p.cluster.GetDeployedPackage("init"); existingInitPackage != nil { + // Use the build version instead of the metadata since this will support older Zarf versions + deprecated.PrintBreakingChanges(existingInitPackage.Data.Build.Version) + } else { + message.Warnf("Unable to retrieve the initialized Zarf version. There is potential for breaking changes.") + } + + spinner.Success() + + return nil +} + // validatePackageArchitecture validates that the package architecture matches the target cluster architecture. func (p *Packager) validatePackageArchitecture() error { - // Ignore this check if the architecture is explicitly "multi" or we don't have a cluster connection - if p.arch == "multi" || p.cluster == nil { + // Ignore this check if the architecture is explicitly "multi", we don't have a cluster connection, or the package contains no images + if p.arch == "multi" || !p.isConnectedToCluster() || !p.hasImages() { return nil } - clusterArch, err := p.cluster.GetArchitecture() + clusterArchitectures, err := p.cluster.GetArchitectures() if err != nil { return lang.ErrUnableToCheckArch } // Check if the package architecture and the cluster architecture are the same. - if p.arch != clusterArch { - return fmt.Errorf(lang.CmdPackageDeployValidateArchitectureErr, p.arch, clusterArch) + if !slices.Contains(clusterArchitectures, p.arch) { + return fmt.Errorf(lang.CmdPackageDeployValidateArchitectureErr, p.arch, strings.Join(clusterArchitectures, ", ")) } return nil diff --git a/src/pkg/packager/common_test.go b/src/pkg/packager/common_test.go index 66fd6cb543..de398a5db4 100644 --- a/src/pkg/packager/common_test.go +++ b/src/pkg/packager/common_test.go @@ -24,7 +24,8 @@ func TestValidatePackageArchitecture(t *testing.T) { type testCase struct { name string pkgArch string - clusterArch string + clusterArchs []string + images []string expectedError error getArchError error } @@ -33,24 +34,41 @@ func TestValidatePackageArchitecture(t *testing.T) { { name: "architecture match", pkgArch: "amd64", - clusterArch: "amd64", + clusterArchs: []string{"amd64"}, + images: []string{"nginx"}, expectedError: nil, }, { name: "architecture mismatch", pkgArch: "arm64", - clusterArch: "amd64", + clusterArchs: []string{"amd64"}, + images: []string{"nginx"}, expectedError: fmt.Errorf(lang.CmdPackageDeployValidateArchitectureErr, "arm64", "amd64"), }, + { + name: "multiple cluster architectures", + pkgArch: "arm64", + clusterArchs: []string{"amd64", "arm64"}, + images: []string{"nginx"}, + expectedError: nil, + }, { name: "ignore validation when package arch equals 'multi'", pkgArch: "multi", - clusterArch: "not evaluated", + clusterArchs: []string{"not evaluated"}, + expectedError: nil, + }, + { + name: "ignore validation when a package doesn't contain images", + pkgArch: "amd64", + images: []string{}, + clusterArchs: []string{"not evaluated"}, expectedError: nil, }, { name: "test the error path when fetching cluster architecture fails", pkgArch: "amd64", + images: []string{"nginx"}, getArchError: errors.New("error fetching cluster architecture"), expectedError: lang.ErrUnableToCheckArch, }, @@ -74,6 +92,15 @@ func TestValidatePackageArchitecture(t *testing.T) { Log: logger, }, }, + cfg: &types.PackagerConfig{ + Pkg: types.ZarfPackage{ + Components: []types.ZarfComponent{ + { + Images: testCase.images, + }, + }, + }, + }, } // Set up test data for fetching cluster architecture. @@ -83,17 +110,21 @@ func TestValidatePackageArchitecture(t *testing.T) { return true, nil, testCase.getArchError } - // Create a NodeList object to fetch cluster architecture with the mock client. - nodeList := &v1.NodeList{ - Items: []v1.Node{ - { - Status: v1.NodeStatus{ - NodeInfo: v1.NodeSystemInfo{ - Architecture: testCase.clusterArch, - }, + nodeItems := []v1.Node{} + + for _, arch := range testCase.clusterArchs { + nodeItems = append(nodeItems, v1.Node{ + Status: v1.NodeStatus{ + NodeInfo: v1.NodeSystemInfo{ + Architecture: arch, }, }, - }, + }) + } + + // Create a NodeList object to fetch cluster architecture with the mock client. + nodeList := &v1.NodeList{ + Items: nodeItems, } return true, nodeList, nil }) diff --git a/src/pkg/packager/components.go b/src/pkg/packager/components.go index a7132c8526..5101f2c0ee 100644 --- a/src/pkg/packager/components.go +++ b/src/pkg/packager/components.go @@ -35,11 +35,11 @@ func (p *Packager) getValidComponents() []types.ZarfComponent { key = component.Name } else { // Otherwise, add the component name to the choice group list for later validation - choiceComponents = p.appendIfNotExists(choiceComponents, component.Name) + choiceComponents = helpers.AppendIfNotExists(choiceComponents, component.Name) } // Preserve component order - orderedKeys = p.appendIfNotExists(orderedKeys, key) + orderedKeys = helpers.AppendIfNotExists(orderedKeys, key) // Append the component to the list of components in the group componentGroups[key] = append(componentGroups[key], component) @@ -65,7 +65,7 @@ func (p *Packager) getValidComponents() []types.ZarfComponent { if requested { // Mark deployment as appliance mode if this is an init config and the k3s component is enabled - if component.Name == k8s.DistroIsK3s && p.cfg.Pkg.Kind == types.ZarfInitConfig { + if component.Name == k8s.DistroIsK3s && p.isInitConfig() { p.cfg.InitOpts.ApplianceMode = true } // Add the component to the list of valid components @@ -208,15 +208,6 @@ func (p *Packager) confirmChoiceGroup(componentGroup []types.ZarfComponent) type return componentGroup[chosen] } -func (p *Packager) appendIfNotExists(slice []string, item string) []string { - for _, s := range slice { - if s == item { - return slice - } - } - return append(slice, item) -} - func (p *Packager) requiresCluster(component types.ZarfComponent) bool { hasImages := len(component.Images) > 0 hasCharts := len(component.Charts) > 0 diff --git a/src/pkg/packager/create.go b/src/pkg/packager/create.go index 02eab1812f..4f1f5854b0 100755 --- a/src/pkg/packager/create.go +++ b/src/pkg/packager/create.go @@ -54,7 +54,7 @@ func (p *Packager) Create() (err error) { } message.Note(fmt.Sprintf("Using build directory %s", p.cfg.CreateOpts.BaseDir)) - if p.cfg.Pkg.Kind == types.ZarfInitConfig { + if p.isInitConfig() { p.cfg.Pkg.Metadata.Version = config.CLIVersion } diff --git a/src/pkg/packager/deploy.go b/src/pkg/packager/deploy.go index b2c350d40c..b48dad3069 100644 --- a/src/pkg/packager/deploy.go +++ b/src/pkg/packager/deploy.go @@ -5,7 +5,6 @@ package packager import ( - "errors" "fmt" "os" "path/filepath" @@ -15,7 +14,6 @@ import ( "time" "github.com/defenseunicorns/zarf/src/config" - "github.com/defenseunicorns/zarf/src/config/lang" "github.com/defenseunicorns/zarf/src/internal/cluster" "github.com/defenseunicorns/zarf/src/internal/packager/git" "github.com/defenseunicorns/zarf/src/internal/packager/helm" @@ -34,16 +32,10 @@ import ( // Deploy attempts to deploy the given PackageConfig. func (p *Packager) Deploy() (err error) { - // Attempt to connect to a Kubernetes cluster. - // Not all packages require Kubernetes, so we only want to log a debug message rather than return the error when we can't connect to a cluster. - p.cluster, err = cluster.NewCluster() - if err != nil { - message.Debug(err) - } - if err = p.source.LoadPackage(p.layout, true); err != nil { return fmt.Errorf("unable to load the package: %w", err) } + if err = p.readZarfYAML(p.layout.ZarfYAML); err != nil { return err } @@ -56,10 +48,6 @@ func (p *Packager) Deploy() (err error) { return err } - if err := p.attemptClusterChecks(); err != nil { - return err - } - // Confirm the overall package deployment if !p.confirmAction(config.ZarfDeployStage) { return fmt.Errorf("deployment cancelled") @@ -74,7 +62,7 @@ func (p *Packager) Deploy() (err error) { p.connectStrings = make(types.ConnectStrings) // Reset registry HPA scale down whether an error occurs or not defer func() { - if p.cluster != nil && p.hpaModified { + if p.isConnectedToCluster() && p.hpaModified { if err := p.cluster.EnableRegHPAScaleDown(); err != nil { message.Debugf("unable to reenable the registry HPA scale down: %s", err.Error()) } @@ -98,31 +86,6 @@ func (p *Packager) Deploy() (err error) { return nil } -// attemptClusterChecks attempts to connect to the cluster and check for useful metadata and config mismatches. -// NOTE: attemptClusterChecks should only return an error if there is a problem significant enough to halt a deployment, otherwise it should return nil and print a warning message. -func (p *Packager) attemptClusterChecks() (err error) { - // Connect to the cluster (if available) to check the Zarf Agent for breaking changes - if p.cluster, _ = cluster.NewCluster(); p.cluster != nil { - - // Check if the package has already been deployed and get its generation - if existingDeployedPackage, _ := p.cluster.GetDeployedPackage(p.cfg.Pkg.Metadata.Name); existingDeployedPackage != nil { - // If this package has been deployed before, increment the package generation within the secret - p.generation = existingDeployedPackage.Generation + 1 - } - - // Check the clusters architecture vs the package spec - if err := p.validatePackageArchitecture(); err != nil { - if errors.Is(err, lang.ErrUnableToCheckArch) { - message.Warnf("Unable to validate package architecture: %s", err.Error()) - } else { - return err - } - } - } - - return nil -} - // deployComponents loops through a list of ZarfComponents and deploys them. func (p *Packager) deployComponents() (deployedComponents []types.DeployedComponent, err error) { componentsToDeploy := p.getValidComponents() @@ -145,8 +108,21 @@ func (p *Packager) deployComponents() (deployedComponents []types.DeployedCompon Status: types.ComponentStatusDeploying, ObservedGeneration: p.generation, } + + // If this component requires a cluster, connect to one + if p.requiresCluster(component) { + timeout := cluster.DefaultTimeout + if p.isInitConfig() { + timeout = 5 * time.Minute + } + + if err := p.connectToCluster(timeout); err != nil { + return deployedComponents, fmt.Errorf("unable to connect to the Kubernetes cluster: %w", err) + } + } + // Ensure we don't overwrite any installedCharts data when updating the package secret - if p.cluster != nil { + if p.isConnectedToCluster() { deployedComponent.InstalledCharts, err = p.cluster.GetInstalledChartsForComponent(p.cfg.Pkg.Metadata.Name, component) if err != nil { message.Debugf("Unable to fetch installed Helm charts for component '%s': %s", component.Name, err.Error()) @@ -157,7 +133,7 @@ func (p *Packager) deployComponents() (deployedComponents []types.DeployedCompon idx := len(deployedComponents) - 1 // Update the package secret to indicate that we are attempting to deploy this component - if p.cluster != nil { + if p.isConnectedToCluster() { if _, err := p.cluster.RecordPackageDeploymentAndWait(p.cfg.Pkg, deployedComponents, p.connectStrings, p.generation, component, p.cfg.DeployOpts.SkipWebhooks); err != nil { message.Debugf("Unable to record package deployment for component %s: this will affect features like `zarf package remove`: %s", component.Name, err.Error()) } @@ -166,7 +142,7 @@ func (p *Packager) deployComponents() (deployedComponents []types.DeployedCompon // Deploy the component var charts []types.InstalledChart var deployErr error - if p.cfg.Pkg.Kind == types.ZarfInitConfig { + if p.isInitConfig() { charts, deployErr = p.deployInitComponent(component) } else { charts, deployErr = p.deployComponent(component, false /* keep img checksum */, false /* always push images */) @@ -185,7 +161,7 @@ func (p *Packager) deployComponents() (deployedComponents []types.DeployedCompon // Update the package secret to indicate that we failed to deploy this component deployedComponents[idx].Status = types.ComponentStatusFailed - if p.cluster != nil { + if p.isConnectedToCluster() { if _, err := p.cluster.RecordPackageDeploymentAndWait(p.cfg.Pkg, deployedComponents, p.connectStrings, p.generation, component, p.cfg.DeployOpts.SkipWebhooks); err != nil { message.Debugf("Unable to record package deployment for component %s: this will affect features like `zarf package remove`: %s", component.Name, err.Error()) } @@ -197,7 +173,7 @@ func (p *Packager) deployComponents() (deployedComponents []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.cluster != nil { + if p.isConnectedToCluster() { if _, err := p.cluster.RecordPackageDeploymentAndWait(p.cfg.Pkg, deployedComponents, p.connectStrings, p.generation, component, p.cfg.DeployOpts.SkipWebhooks); err != nil { message.Debugf("Unable to record package deployment for component %s: this will affect features like `zarf package remove`: %s", component.Name, err.Error()) } @@ -221,11 +197,6 @@ func (p *Packager) deployInitComponent(component types.ZarfComponent) (charts [] // Always init the state before the first component that requires the cluster (on most deployments, the zarf-seed-registry) if p.requiresCluster(component) && p.cfg.State == nil { - p.cluster, err = cluster.NewClusterWithWait(5 * time.Minute) - if err != nil { - return charts, fmt.Errorf("unable to connect to the Kubernetes cluster: %w", err) - } - err = p.cluster.InitZarfState(p.cfg.InitOpts) if err != nil { return charts, fmt.Errorf("unable to initialize Zarf state: %w", err) @@ -290,15 +261,8 @@ func (p *Packager) deployComponent(component types.ZarfComponent, noImgChecksum } if !p.valueTemplate.Ready() && p.requiresCluster(component) { - // Make sure we have access to the cluster - if p.cluster == nil { - p.cluster, err = cluster.NewClusterWithWait(cluster.DefaultTimeout) - if err != nil { - return charts, fmt.Errorf("unable to connect to the Kubernetes cluster: %w", err) - } - } // Setup the state in the config and get the valuesTemplate - p.valueTemplate, err = p.setupStateValuesTemplate(component) + p.valueTemplate, err = p.setupStateValuesTemplate() if err != nil { return charts, fmt.Errorf("unable to get the updated value template: %w", err) } @@ -427,8 +391,8 @@ func (p *Packager) processComponentFiles(component types.ZarfComponent, pkgLocat return nil } -// Fetch the current ZarfState from the k8s cluster and generate a p.valueTemplate from the state values. -func (p *Packager) setupStateValuesTemplate(component types.ZarfComponent) (values *template.Values, err error) { +// setupStateValuesTemplate fetched the current ZarfState from the k8s cluster and generate a p.valueTemplate from the state values. +func (p *Packager) setupStateValuesTemplate() (values *template.Values, err error) { // If we are touching K8s, make sure we can talk to it once per deployment spinner := message.NewProgressSpinner("Loading the Zarf State from the Kubernetes cluster") defer spinner.Stop() @@ -464,13 +428,6 @@ func (p *Packager) setupStateValuesTemplate(component types.ZarfComponent) (valu return values, err } - // Only check the architecture if the package has images - if len(component.Images) > 0 && state.Architecture != p.arch { - // If the package has images but the architectures don't match, fail the deployment and warn the user to avoid ugly hidden errors with image push/pull - return values, fmt.Errorf("this package architecture is %s, but this cluster seems to be initialized with the %s architecture", - p.arch, state.Architecture) - } - spinner.Success() return values, nil } @@ -515,7 +472,14 @@ func (p *Packager) pushReposToRepository(reposPath string, repos []string) error svcInfo, _ := k8s.ServiceInfoFromServiceURL(gitClient.Server.Address) // If this is a service (svcInfo is not nil), create a port-forward tunnel to that resource - if svcInfo != nil && p.cluster != nil { + if svcInfo != nil { + if !p.isConnectedToCluster() { + err := p.connectToCluster(5 * time.Second) + if err != nil { + return err + } + } + tunnel, err := p.cluster.NewTunnel(svcInfo.Namespace, k8s.SvcResource, svcInfo.Name, "", 0, svcInfo.Port) if err != nil { return err @@ -629,7 +593,7 @@ func (p *Packager) printTablesForDeployment(componentsToDeploy []types.DeployedC pterm.Println() // If not init config, print the application connection table - if !(p.cfg.Pkg.Kind == types.ZarfInitConfig) { + if !p.isInitConfig() { message.PrintConnectStringTable(p.connectStrings) } else { // Grab a fresh copy of the state (if we are able) to print the most up-to-date version of the creds diff --git a/src/pkg/packager/deprecated/common.go b/src/pkg/packager/deprecated/common.go index fdffb3ba71..2b036585ee 100644 --- a/src/pkg/packager/deprecated/common.go +++ b/src/pkg/packager/deprecated/common.go @@ -81,8 +81,6 @@ func MigrateComponent(build types.ZarfBuildData, component types.ZarfComponent) func PrintBreakingChanges(deployedZarfVersion string) { deployedSemver, err := semver.NewVersion(deployedZarfVersion) if err != nil { - message.HorizontalRule() - pterm.Println() message.Warnf("Unable to determine init-package version from %s. There is potential for breaking changes.", deployedZarfVersion) return } @@ -119,5 +117,7 @@ func PrintBreakingChanges(deployedZarfVersion string) { pterm.Printfln("\n - %s", pterm.Bold.Sprint("Mitigation:")) pterm.Printfln(" %s", strings.ReplaceAll(mitigationText, "\n", "\n ")) } + + message.HorizontalRule() } } diff --git a/src/pkg/packager/mirror.go b/src/pkg/packager/mirror.go index a273287d03..293663a97c 100644 --- a/src/pkg/packager/mirror.go +++ b/src/pkg/packager/mirror.go @@ -11,7 +11,6 @@ import ( "slices" "github.com/defenseunicorns/zarf/src/config" - "github.com/defenseunicorns/zarf/src/internal/cluster" "github.com/defenseunicorns/zarf/src/pkg/message" "github.com/defenseunicorns/zarf/src/pkg/utils/helpers" "github.com/defenseunicorns/zarf/src/types" @@ -19,13 +18,6 @@ import ( // Mirror pulls resources from a package (images, git repositories, etc) and pushes them to remotes in the air gap without deploying them func (p *Packager) Mirror() (err error) { - // Attempt to connect to a Kubernetes cluster. - // Not all packages require Kubernetes, so we only want to log a debug message rather than return the error when we can't connect to a cluster. - p.cluster, err = cluster.NewCluster() - if err != nil { - message.Debug(err) - } - spinner := message.NewProgressSpinner("Mirroring Zarf package %s", p.cfg.PkgOpts.PackageSource) defer spinner.Stop() diff --git a/src/pkg/packager/publish.go b/src/pkg/packager/publish.go index 1947e0b1cf..899f0d77bc 100644 --- a/src/pkg/packager/publish.go +++ b/src/pkg/packager/publish.go @@ -133,7 +133,7 @@ func (p *Packager) loadSkeleton() (err error) { return fmt.Errorf("unable to read the zarf.yaml file: %s", err.Error()) } - if p.cfg.Pkg.Kind == types.ZarfInitConfig { + if p.isInitConfig() { p.cfg.Pkg.Metadata.Version = config.CLIVersion } diff --git a/src/pkg/packager/remove.go b/src/pkg/packager/remove.go index 7334c7b3da..7ccb55cfdd 100644 --- a/src/pkg/packager/remove.go +++ b/src/pkg/packager/remove.go @@ -68,11 +68,9 @@ func (p *Packager) Remove() (err error) { deployedPackage := &types.DeployedPackage{} if requiresCluster { - if p.cluster == nil { - p.cluster, err = cluster.NewClusterWithWait(cluster.DefaultTimeout) - if err != nil { - return err - } + err = p.connectToCluster(cluster.DefaultTimeout) + if err != nil { + return err } deployedPackage, err = p.cluster.GetDeployedPackage(packageName) if err != nil { diff --git a/src/pkg/packager/sources/tarball.go b/src/pkg/packager/sources/tarball.go index 2a2b5ee666..fd6f5f002d 100644 --- a/src/pkg/packager/sources/tarball.go +++ b/src/pkg/packager/sources/tarball.go @@ -34,7 +34,8 @@ type TarballSource struct { func (s *TarballSource) LoadPackage(dst *layout.PackagePaths, unarchiveAll bool) (err error) { var pkg types.ZarfPackage - message.Debugf("Loading package from %q", s.PackageSource) + spinner := message.NewProgressSpinner("Loading package from %q", s.PackageSource) + defer spinner.Stop() if s.Shasum != "" { if err := utils.SHAsMatch(s.PackageSource, s.Shasum); err != nil { @@ -122,6 +123,8 @@ func (s *TarballSource) LoadPackage(dst *layout.PackagePaths, unarchiveAll bool) } } + spinner.Success() + return nil } diff --git a/src/pkg/utils/helpers/misc.go b/src/pkg/utils/helpers/misc.go index 06f168c1d2..fc74aca9b7 100644 --- a/src/pkg/utils/helpers/misc.go +++ b/src/pkg/utils/helpers/misc.go @@ -193,3 +193,13 @@ func StringToSlice(s string) []string { return []string{} } + +// AppendIfNotExists appends a string to a slice of strings if it is not present already on the slice. +func AppendIfNotExists(slice []string, item string) []string { + for _, s := range slice { + if s == item { + return slice + } + } + return append(slice, item) +} diff --git a/src/test/e2e/29_mismatched_architectures_test.go b/src/test/e2e/29_mismatched_architectures_test.go deleted file mode 100644 index efcde0b225..0000000000 --- a/src/test/e2e/29_mismatched_architectures_test.go +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: 2021-Present The Zarf Authors - -// Package test provides e2e tests for Zarf. -package test - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/require" -) - -// TestMismatchedArchitectures ensures that zarf produces an error -// when the package architecture doesn't match the target cluster architecture. -func TestMismatchedArchitectures(t *testing.T) { - t.Log("E2E: Mismatched architectures") - e2e.SetupWithCluster(t) - - var ( - mismatchedArch = e2e.GetMismatchedArch() - mismatchedGamesPackage = fmt.Sprintf("zarf-package-dos-games-%s-1.0.0.tar.zst", mismatchedArch) - expectedErrorMessage = fmt.Sprintf("this package architecture is %s", mismatchedArch) - ) - - // Build dos-games package with different arch than the cluster arch. - stdOut, stdErr, err := e2e.Zarf("package", "create", "examples/dos-games/", "--architecture", mismatchedArch, "--confirm") - require.NoError(t, err, stdOut, stdErr) - defer e2e.CleanFiles(mismatchedGamesPackage) - - // Ensure zarf package deploy returns an error because of the mismatched architectures. - _, stdErr, err = e2e.Zarf("package", "deploy", mismatchedGamesPackage, "--confirm") - require.Error(t, err, stdErr) - require.Contains(t, stdErr, expectedErrorMessage) -} diff --git a/src/test/e2e/29_mismatched_checks_test.go b/src/test/e2e/29_mismatched_checks_test.go new file mode 100644 index 0000000000..2ce08b44c0 --- /dev/null +++ b/src/test/e2e/29_mismatched_checks_test.go @@ -0,0 +1,90 @@ +// 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" + "testing" + + "github.com/defenseunicorns/zarf/src/types" + "github.com/stretchr/testify/require" +) + +// TestMismatchedArchitectures ensures that zarf produces an error +// when the package architecture doesn't match the target cluster architecture. +func TestMismatchedArchitectures(t *testing.T) { + t.Log("E2E: Mismatched architectures") + e2e.SetupWithCluster(t) + + var ( + mismatchedArch = e2e.GetMismatchedArch() + mismatchedGamesPackage = fmt.Sprintf("zarf-package-dos-games-%s-1.0.0.tar.zst", mismatchedArch) + expectedErrorMessage = fmt.Sprintf("this package architecture is %s", mismatchedArch) + ) + + // Build dos-games package with different arch than the cluster arch. + stdOut, stdErr, err := e2e.Zarf("package", "create", "examples/dos-games/", "--architecture", mismatchedArch, "--confirm") + require.NoError(t, err, stdOut, stdErr) + defer e2e.CleanFiles(mismatchedGamesPackage) + + // Ensure zarf package deploy returns an error because of the mismatched architectures. + _, stdErr, err = e2e.Zarf("package", "deploy", mismatchedGamesPackage, "--confirm") + require.Error(t, err, stdErr) + require.Contains(t, stdErr, expectedErrorMessage) +} + +// TestMismatchedVersions ensures that zarf produces a warning +// when the initialized version of Zarf doesn't match the current CLI +func TestMismatchedVersions(t *testing.T) { + t.Log("E2E: Mismatched versions") + e2e.SetupWithCluster(t) + + var ( + expectedWarningMessage = "Potential Breaking Changes" + ) + + // Get the current init package secret + initPkg := types.DeployedPackage{} + base64Pkg, _, err := e2e.Kubectl("get", "secret", "zarf-package-init", "-n", "zarf", "-o", "jsonpath={.data.data}") + require.NoError(t, err) + jsonPkg, err := base64.StdEncoding.DecodeString(base64Pkg) + require.NoError(t, err) + fmt.Println(string(jsonPkg)) + err = json.Unmarshal(jsonPkg, &initPkg) + require.NoError(t, err) + + // Edit the build data to trigger the breaking change check + initPkg.Data.Build.Version = "v0.25.0" + + // Delete the package secret + _, _, err = e2e.Kubectl("delete", "secret", "zarf-package-init", "-n", "zarf") + require.NoError(t, err) + + // Create a new secret with the modified data + jsonPkgModified, err := json.Marshal(initPkg) + require.NoError(t, err) + _, _, err = e2e.Kubectl("create", "secret", "generic", "zarf-package-init", "-n", "zarf", fmt.Sprintf("--from-literal=data=%s", string(jsonPkgModified))) + require.NoError(t, err) + + path := filepath.Join("build", fmt.Sprintf("zarf-package-dos-games-%s-1.0.0.tar.zst", e2e.Arch)) + + // Deploy the games package + stdOut, stdErr, err := e2e.Zarf("package", "deploy", path, "--confirm") + require.NoError(t, err, stdOut, stdErr) + require.Contains(t, stdErr, expectedWarningMessage) + + // Remove the games package + stdOut, stdErr, err = e2e.Zarf("package", "remove", "dos-games", "--confirm") + require.NoError(t, err, stdOut, stdErr) + + // Reset the package secret + _, _, err = e2e.Kubectl("delete", "secret", "zarf-package-init", "-n", "zarf") + require.NoError(t, err) + _, _, err = e2e.Kubectl("create", "secret", "generic", "zarf-package-init", "-n", "zarf", fmt.Sprintf("--from-literal=data=%s", string(jsonPkg))) + require.NoError(t, err) +}