Skip to content

Commit

Permalink
Fail when package architecture doesn't match cluster architecture (#1495
Browse files Browse the repository at this point in the history
)

## Description
Fail `zarf init` and `zarf package deploy` when package architecture
doesn't match cluster architecture

Deployment scenarios that this PR covers:

- If `zarf init` is ran in appliance mode(deploying k3s), verify that
the package architecture matches the machine that zarf is attempting to
initialize with a k3s cluster

- If `zarf init` or `zarf package deploy` is ran, verify that the
package architecture matches the target cluster architecture

## Related Issue

Fixes #1491

Relates to #955

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## Checklist before merging

- [x] Test, docs, adr added or updated as needed
- [x] [Contributor Guide
Steps](https://github.com/defenseunicorns/zarf/blob/main/CONTRIBUTING.md#developer-workflow)
followed

---------

Co-authored-by: Wayne Starr <[email protected]>
  • Loading branch information
lucasrod16 and Racer159 committed May 8, 2023
1 parent 5fcdc3c commit 84fb520
Show file tree
Hide file tree
Showing 11 changed files with 126 additions and 17 deletions.
12 changes: 12 additions & 0 deletions packages/distros/k3s/zarf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
3 changes: 1 addition & 2 deletions src/cmd/destroy.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"errors"
"os"
"regexp"
"time"

"github.com/defenseunicorns/zarf/src/config"
"github.com/defenseunicorns/zarf/src/config/lang"
Expand All @@ -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)
}
Expand Down
1 change: 1 addition & 0 deletions src/config/lang/english.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
5 changes: 3 additions & 2 deletions src/internal/cluster/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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")
}
Expand Down
6 changes: 3 additions & 3 deletions src/internal/cluster/tunnel.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
}
Expand Down
19 changes: 19 additions & 0 deletions src/pkg/packager/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion src/pkg/packager/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
}
Expand Down
3 changes: 1 addition & 2 deletions src/pkg/packager/remove.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"encoding/json"
"fmt"
"strings"
"time"

"github.com/defenseunicorns/zarf/src/config"
"github.com/defenseunicorns/zarf/src/internal/cluster"
Expand Down Expand Up @@ -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)
}
Expand Down
11 changes: 11 additions & 0 deletions src/test/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
28 changes: 21 additions & 7 deletions src/test/e2e/20_zarf_init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@
package test

import (
"context"
"fmt"
"testing"
"time"

"github.com/defenseunicorns/zarf/src/pkg/utils/exec"
"github.com/stretchr/testify/require"
)

Expand All @@ -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")

Expand Down
49 changes: 49 additions & 0 deletions src/test/e2e/29_mismatched_architectures_test.go
Original file line number Diff line number Diff line change
@@ -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)
}

0 comments on commit 84fb520

Please sign in to comment.