diff --git a/packages/distros/k3s/zarf.yaml b/packages/distros/k3s/zarf.yaml index f8a2c0897a..4cfb478d7b 100644 --- a/packages/distros/k3s/zarf.yaml +++ b/packages/distros/k3s/zarf.yaml @@ -27,6 +27,12 @@ components: - source: https://github.com/k3s-io/k3s/releases/download/v1.24.1+k3s1/k3s-airgap-images-amd64.tar.zst shasum: 6736f9fa4d5754d60b0508bafb2f888170cb99a2d93a3a1617a919ca4ee74034 target: /var/lib/rancher/k3s/agent/images/k3s.tar.zst + actions: + onDeploy: + before: + - cmd: if [ "$(arch)" != "x86_64" ]; then echo "this package architecture is amd64, but the target system has a different architecture. These architectures must be the same" && exit 1; fi + description: Check that the host architecture matches the package architecture + maxRetries: 0 # ARM-64 version of the K3s stack - name: k3s @@ -51,3 +57,9 @@ components: - source: https://github.com/k3s-io/k3s/releases/download/v1.24.1+k3s1/k3s-airgap-images-arm64.tar.zst shasum: 12029e4bbfecfa16942345aeac798f4790e568a7202c2b85ee068d7b4756ba04 target: /var/lib/rancher/k3s/agent/images/k3s.tar.zst + actions: + onDeploy: + before: + - cmd: if [ "$(arch)" != "arm64" ]; then echo "this package architecture is arm64, but the target system has a different architecture. These architectures must be the same" && exit 1; fi + description: Check that the host architecture matches the package architecture + maxRetries: 0 \ No newline at end of file diff --git a/src/cmd/destroy.go b/src/cmd/destroy.go index 512a576fb2..336a24ace6 100644 --- a/src/cmd/destroy.go +++ b/src/cmd/destroy.go @@ -8,7 +8,6 @@ import ( "errors" "os" "regexp" - "time" "github.com/defenseunicorns/zarf/src/config" "github.com/defenseunicorns/zarf/src/config/lang" @@ -30,7 +29,7 @@ var destroyCmd = &cobra.Command{ Short: lang.CmdDestroyShort, Long: lang.CmdDestroyLong, Run: func(cmd *cobra.Command, args []string) { - c, err := cluster.NewClusterWithWait(30*time.Second, true) + c, err := cluster.NewClusterWithWait(cluster.DefaultTimeout, true) if err != nil { message.Fatalf(err, lang.ErrNoClusterConnection) } diff --git a/src/config/lang/english.go b/src/config/lang/english.go index 52f443d992..24ec7f5c2c 100644 --- a/src/config/lang/english.go +++ b/src/config/lang/english.go @@ -225,6 +225,7 @@ zarf init --git-push-password={PASSWORD} --git-push-username={USERNAME} --git-ur CmdPackageDeployFlagShasum = "Shasum of the package to deploy. Required if deploying a remote package and \"--insecure\" is not provided" CmdPackageDeployFlagSget = "Path to public sget key file for remote packages signed via cosign" CmdPackageDeployFlagPublicKey = "Path to public key file for validating signed packages" + CmdPackageDeployValidateArchitectureErr = "this package architecture is %s, but the target cluster has the %s architecture. These architectures must be the same" CmdPackageInspectFlagSbom = "View SBOM contents while inspecting the package" CmdPackageInspectFlagSbomOut = "Specify an output directory for the SBOMs from the inspected Zarf package" diff --git a/src/internal/cluster/common.go b/src/internal/cluster/common.go index 3c78c3236c..11bfa7a3e9 100644 --- a/src/internal/cluster/common.go +++ b/src/internal/cluster/common.go @@ -18,7 +18,8 @@ type Cluster struct { } const ( - defaultTimeout = 30 * time.Second + // DefaultTimeout is the default time to wait for a cluster to be ready. + DefaultTimeout = 30 * time.Second agentLabel = "zarf.dev/agent" ) @@ -28,7 +29,7 @@ var labels = k8s.Labels{ // NewClusterOrDie creates a new cluster instance and waits up to 30 seconds for the cluster to be ready or throws a fatal error. func NewClusterOrDie() *Cluster { - c, err := NewClusterWithWait(defaultTimeout, true) + c, err := NewClusterWithWait(DefaultTimeout, true) if err != nil { message.Fatalf(err, "Failed to connect to cluster") } diff --git a/src/internal/cluster/tunnel.go b/src/internal/cluster/tunnel.go index 3d9a5eeb3e..f753554914 100644 --- a/src/internal/cluster/tunnel.go +++ b/src/internal/cluster/tunnel.go @@ -124,7 +124,7 @@ func ServiceInfoFromNodePortURL(nodePortURL string) (*ServiceInfo, error) { return nil, fmt.Errorf("node port services should use the port range 30000-32767") } - kube, err := k8s.NewWithWait(message.Debugf, labels, defaultTimeout) + kube, err := k8s.NewWithWait(message.Debugf, labels, DefaultTimeout) if err != nil { return nil, err } @@ -187,7 +187,7 @@ func ServiceInfoFromServiceURL(serviceURL string) (*ServiceInfo, error) { func NewTunnel(namespace, resourceType, resourceName string, local, remote int) (*Tunnel, error) { message.Debugf("tunnel.NewTunnel(%s, %s, %s, %d, %d)", namespace, resourceType, resourceName, local, remote) - kube, err := k8s.NewWithWait(message.Debugf, labels, defaultTimeout) + kube, err := k8s.NewWithWait(message.Debugf, labels, DefaultTimeout) if err != nil { return &Tunnel{}, err } @@ -410,7 +410,7 @@ func (tunnel *Tunnel) establish() (string, error) { message.Debug(spinnerMessage) } - kube, err := k8s.NewWithWait(message.Debugf, labels, defaultTimeout) + kube, err := k8s.NewWithWait(message.Debugf, labels, DefaultTimeout) if err != nil { return "", fmt.Errorf("unable to connect to the cluster: %w", err) } diff --git a/src/pkg/packager/common.go b/src/pkg/packager/common.go index 6a8da056fb..15c202a48a 100644 --- a/src/pkg/packager/common.go +++ b/src/pkg/packager/common.go @@ -16,6 +16,7 @@ import ( "strings" "github.com/AlecAivazis/survey/v2" + "github.com/defenseunicorns/zarf/src/config/lang" "github.com/defenseunicorns/zarf/src/internal/cluster" "github.com/defenseunicorns/zarf/src/internal/packager/sbom" "github.com/defenseunicorns/zarf/src/types" @@ -443,6 +444,24 @@ func (p *Packager) validatePackageChecksums() error { return nil } +// validatePackageArchitecture validates that the package architecture matches the target cluster architecture. +func (p *Packager) validatePackageArchitecture() error { + // Attempt to connect to a cluster to get the architecture. + if cluster, err := cluster.NewCluster(); err == nil { + clusterArch, err := cluster.Kube.GetArchitecture() + if err != nil { + return err + } + + // Check if the package architecture and the cluster architecture are the same. + if p.arch != clusterArch { + return fmt.Errorf(lang.CmdPackageDeployValidateArchitectureErr, p.arch, clusterArch) + } + } + + return nil +} + func (p *Packager) validatePackageSignature(publicKeyPath string) error { // If the insecure flag was provided, ignore the signature validation diff --git a/src/pkg/packager/deploy.go b/src/pkg/packager/deploy.go index 4bb5411a3b..c051772c68 100644 --- a/src/pkg/packager/deploy.go +++ b/src/pkg/packager/deploy.go @@ -42,6 +42,10 @@ func (p *Packager) Deploy() error { return fmt.Errorf("unable to load the Zarf Package: %w", err) } + if err := p.validatePackageArchitecture(); err != nil { + return err + } + if err := p.validatePackageSignature(p.cfg.DeployOpts.PublicKeyPath); err != nil { return err } @@ -218,7 +222,7 @@ func (p *Packager) deployComponent(component types.ZarfComponent, noImgChecksum // Make sure we have access to the cluster if p.cluster == nil { - p.cluster, err = cluster.NewClusterWithWait(30*time.Second, true) + p.cluster, err = cluster.NewClusterWithWait(cluster.DefaultTimeout, true) if err != nil { return charts, fmt.Errorf("unable to connect to the Kubernetes cluster: %w", err) } diff --git a/src/pkg/packager/remove.go b/src/pkg/packager/remove.go index 24170dea98..4013cfb43b 100644 --- a/src/pkg/packager/remove.go +++ b/src/pkg/packager/remove.go @@ -8,7 +8,6 @@ import ( "encoding/json" "fmt" "strings" - "time" "github.com/defenseunicorns/zarf/src/config" "github.com/defenseunicorns/zarf/src/internal/cluster" @@ -64,7 +63,7 @@ func (p *Packager) Remove(packageName string) (err error) { if requiresCluster { // If we need the cluster, connect to it and pull the package secret if p.cluster == nil { - p.cluster, err = cluster.NewClusterWithWait(30*time.Second, true) + p.cluster, err = cluster.NewClusterWithWait(cluster.DefaultTimeout, true) if err != nil { return fmt.Errorf("unable to connect to the Kubernetes cluster: %w", err) } diff --git a/src/test/common.go b/src/test/common.go index 5f968ec9bb..c42b5a070b 100644 --- a/src/test/common.go +++ b/src/test/common.go @@ -75,3 +75,14 @@ func (e2e *ZarfE2ETest) CleanFiles(files ...string) { _ = os.RemoveAll(file) } } + +// GetMismatchedArch determines what architecture our tests are running on, +// and returns the opposite architecture. +func (e2e *ZarfE2ETest) GetMismatchedArch() string { + switch e2e.Arch { + case "arm64": + return "amd64" + default: + return"arm64" + } +} diff --git a/src/test/e2e/20_zarf_init_test.go b/src/test/e2e/20_zarf_init_test.go index cabbb49afe..37114d12a9 100644 --- a/src/test/e2e/20_zarf_init_test.go +++ b/src/test/e2e/20_zarf_init_test.go @@ -5,11 +5,9 @@ package test import ( - "context" + "fmt" "testing" - "time" - "github.com/defenseunicorns/zarf/src/pkg/utils/exec" "github.com/stretchr/testify/require" ) @@ -24,16 +22,32 @@ func TestZarfInit(t *testing.T) { initComponents = "k3s,logging,git-server" } - ctx, cancel := context.WithTimeout(context.TODO(), 10*time.Minute) - defer cancel() + var ( + mismatchedArch = e2e.GetMismatchedArch() + initPackageVersion = "UnknownVersion" + mismatchedInitPackage = fmt.Sprintf("zarf-init-%s-%s.tar.zst", mismatchedArch, initPackageVersion) + expectedErrorMessage = fmt.Sprintf("this package architecture is %s", mismatchedArch) + ) + + // Build init package with different arch than the cluster arch. + stdOut, stdErr, err := e2e.ExecZarfCommand("package", "create", ".", "--architecture", mismatchedArch, "--confirm") + require.NoError(t, err, stdOut, stdErr) + defer e2e.CleanFiles(mismatchedInitPackage) + + // Check that `zarf init` fails in appliance mode when we try to initialize a k3s cluster + // on a machine with a different architecture than the package architecture. + // We need to use the --architecture flag here to force zarf to find the package. + _, stdErr, err = e2e.ExecZarfCommand("init", "--architecture", mismatchedArch, "--components=k3s", "--confirm") + require.Error(t, err, stdErr) + require.Contains(t, stdErr, expectedErrorMessage) // run `zarf init` - _, stdErr, err := exec.CmdWithContext(ctx, exec.PrintCfg(), e2e.ZarfBinPath, "init", "--components="+initComponents, "--confirm", "--nodeport", "31337") + _, stdErr, err = e2e.ExecZarfCommand("init", "--components="+initComponents, "--confirm", "--nodeport", "31337") require.Contains(t, stdErr, "artifacts with software bill-of-materials (SBOM) included") require.NoError(t, err) // Check that gitea is actually running and healthy - stdOut, _, err := e2e.ExecZarfCommand("tools", "kubectl", "get", "pods", "-l", "app in (gitea)", "-n", "zarf", "-o", "jsonpath={.items[*].status.phase}") + stdOut, _, err = e2e.ExecZarfCommand("tools", "kubectl", "get", "pods", "-l", "app in (gitea)", "-n", "zarf", "-o", "jsonpath={.items[*].status.phase}") require.NoError(t, err) require.Contains(t, stdOut, "Running") diff --git a/src/test/e2e/29_mismatched_architectures_test.go b/src/test/e2e/29_mismatched_architectures_test.go new file mode 100644 index 0000000000..f3e897a720 --- /dev/null +++ b/src/test/e2e/29_mismatched_architectures_test.go @@ -0,0 +1,49 @@ +// 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) + defer e2e.Teardown(t) + + var ( + mismatchedArch = e2e.GetMismatchedArch() + mismatchedGamesPackage = fmt.Sprintf("zarf-package-dos-games-%s.tar.zst", mismatchedArch) + initPackageVersion = "UnknownVersion" + mismatchedInitPackage = fmt.Sprintf("zarf-init-%s-%s.tar.zst", mismatchedArch, initPackageVersion) + expectedErrorMessage = fmt.Sprintf("this package architecture is %s", mismatchedArch) + ) + + // Build init package with different arch than the cluster arch. + stdOut, stdErr, err := e2e.ExecZarfCommand("package", "create", ".", "--architecture", mismatchedArch, "--confirm") + require.NoError(t, err, stdOut, stdErr) + defer e2e.CleanFiles(mismatchedInitPackage) + + // Build dos-games package with different arch than the cluster arch. + stdOut, stdErr, err = e2e.ExecZarfCommand("package", "create", "examples/dos-games/", "--architecture", mismatchedArch, "--confirm") + require.NoError(t, err, stdOut, stdErr) + defer e2e.CleanFiles(mismatchedGamesPackage) + + // Ensure zarf init returns an error because of the mismatched architectures. + // We need to use the --architecture flag here to force zarf to find the package. + _, stdErr, err = e2e.ExecZarfCommand("init", "--architecture", mismatchedArch, "--confirm") + require.Error(t, err, stdErr) + require.Contains(t, stdErr, expectedErrorMessage) + + // Ensure zarf package deploy returns an error because of the mismatched architectures. + _, stdErr, err = e2e.ExecZarfCommand("package", "deploy", mismatchedGamesPackage, "--confirm") + require.Error(t, err, stdErr) + require.Contains(t, stdErr, expectedErrorMessage) +}