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
11 changes: 8 additions & 3 deletions src/internal/api/v1alpha1/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,17 @@ func ValidatePackage(pkg v1alpha1.ZarfPackage) error {
}
}
}
// A skeleton retains one component per architecture/flavor, so names need only be unique per variant there.
isSkeleton := pkg.Metadata.Architecture == v1alpha1.SkeletonArch
for _, component := range pkg.Components {
// ensure component name is unique
if _, ok := uniqueComponentNames[component.Name]; ok {
nameKey := component.Name
if isSkeleton {
nameKey = strings.Join([]string{component.Name, component.Only.Cluster.Architecture, component.Only.Flavor}, "\x00")
}
if _, ok := uniqueComponentNames[nameKey]; ok {
err = errors.Join(err, fmt.Errorf(PkgValidateErrComponentNameNotUnique, component.Name))
}
uniqueComponentNames[component.Name] = true
uniqueComponentNames[nameKey] = true
if component.IsRequired() {
if component.Default {
err = errors.Join(err, fmt.Errorf(PkgValidateErrComponentReqDefault, component.Name))
Expand Down
33 changes: 18 additions & 15 deletions src/pkg/packager/load/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,17 +152,18 @@ func resolveImports(ctx context.Context, pkg v1alpha1.ZarfPackage, packagePath,

name := getComponentToImportName(component)
found := []v1alpha1.ZarfComponent{}
for _, component := range importedPkg.Components {
if component.Name == name && compatibleComponent(component, arch, flavor) {
found = append(found, component)
for _, importedComponent := range importedPkg.Components {
if importedComponent.Name == name && compatibleComponent(importedComponent, arch, flavor) {
found = append(found, importedComponent)
}
}
if len(found) == 0 {
return v1alpha1.ZarfPackage{}, nil, fmt.Errorf("no compatible component named %s found", name)
} else if len(found) > 1 {
}
// A concrete-arch build resolves to one component; a skeleton retains every arch variant.
if len(found) > 1 && arch != v1alpha1.SkeletonArch {
return v1alpha1.ZarfPackage{}, nil, fmt.Errorf("multiple components named %s found", name)
}
importedComponent := found[0]

importPath, err := fetchOCISkeleton(ctx, component, pkgPath.BaseDir, cachePath, remoteOptions)
if err != nil {
Expand All @@ -178,16 +179,18 @@ func resolveImports(ctx context.Context, pkg v1alpha1.ZarfPackage, packagePath,
if !fileInfo.IsDir() {
importPath = filepath.Dir(importPath)
}
importedComponent = fixPaths(importedComponent, importPath, pkgPath.BaseDir)
composed, err := overrideMetadata(importedComponent, component)
if err != nil {
return v1alpha1.ZarfPackage{}, nil, err
}
composed = overrideDeprecated(composed, component)
composed = overrideActions(composed, component)
composed = overrideResources(composed, component)

components = append(components, composed)
for _, importedComponent := range found {
importedComponent = fixPaths(importedComponent, importPath, pkgPath.BaseDir)
composed, err := overrideMetadata(importedComponent, component)
if err != nil {
return v1alpha1.ZarfPackage{}, nil, err
}
composed = overrideDeprecated(composed, component)
composed = overrideActions(composed, component)
composed = overrideResources(composed, component)
components = append(components, composed)
}
variables = append(variables, importedPkg.Variables...)
constants = append(constants, importedPkg.Constants...)
for _, v := range importedPkg.Values.Files {
Expand Down Expand Up @@ -278,7 +281,7 @@ func validateComponentCompose(c v1alpha1.ZarfComponent) error {
}

func compatibleComponent(c v1alpha1.ZarfComponent, arch, flavor string) bool {
satisfiesArch := c.Only.Cluster.Architecture == "" || c.Only.Cluster.Architecture == arch
satisfiesArch := c.Only.Cluster.Architecture == "" || arch == v1alpha1.SkeletonArch || c.Only.Cluster.Architecture == arch
satisfiesFlavor := c.Only.Flavor == "" || c.Only.Flavor == flavor
return satisfiesArch && satisfiesFlavor
}
Expand Down
49 changes: 49 additions & 0 deletions src/pkg/packager/load/import_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -521,3 +521,52 @@ func TestCompatibleComponent(t *testing.T) {
})
}
}

func TestCompatibleComponentSkeletonArch(t *testing.T) {
t.Parallel()

for _, arch := range []string{"amd64", "arm64"} {
c := v1alpha1.ZarfComponent{
Only: v1alpha1.ZarfComponentOnlyTarget{
Cluster: v1alpha1.ZarfComponentOnlyCluster{Architecture: arch},
},
}
require.True(t, compatibleComponent(c, v1alpha1.SkeletonArch, ""),
"skeleton compose must retain %s component", arch)
}
}

func TestResolveImportsSkeletonRetainsAllArches(t *testing.T) {

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.

This test shouldn't be focused on skeletons (publish_test.go is handling that). Make this test ensure that the variant dimensions work separately and together

t.Parallel()
ctx := testutil.TestContext(t)

dir := t.TempDir()
zarfYAML := `kind: ZarfPackageConfig
metadata:
name: arch-skeleton
components:
- name: injector
only:
cluster:
architecture: amd64
- name: injector
only:
cluster:
architecture: arm64
`
require.NoError(t, os.WriteFile(filepath.Join(dir, layout.ZarfYAML), []byte(zarfYAML), 0o644))
b, err := os.ReadFile(filepath.Join(dir, layout.ZarfYAML))
require.NoError(t, err)
pkg, err := pkgcfg.Parse(ctx, b)
require.NoError(t, err)

resolved, _, err := resolveImports(ctx, pkg, dir, v1alpha1.SkeletonArch, "", []string{}, "", false, types.RemoteOptions{})
require.NoError(t, err)

archs := []string{}
for _, c := range resolved.Components {
archs = append(archs, c.Only.Cluster.Architecture)
}
require.ElementsMatch(t, []string{"amd64", "arm64"}, archs,
"skeleton must retain all only.cluster.architecture variants")
}
26 changes: 25 additions & 1 deletion src/pkg/packager/load/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"fmt"
"os"
"path/filepath"
"slices"
"time"

"github.com/zarf-dev/zarf/src/api/v1alpha1"
Expand All @@ -26,6 +27,21 @@ import (
"github.com/zarf-dev/zarf/src/types"
)

// VariantDimension is a dimension along which components of the same name may vary.
type VariantDimension string

const (
// VariantArchitecture is the only.cluster.architecture dimension of a component.
VariantArchitecture VariantDimension = "architecture"
// VariantFlavor is the only.flavor dimension of a component.
VariantFlavor VariantDimension = "flavor"
)

// AllVariantDimension returns every variant dimension.
func AllVariantDimension() []VariantDimension {
return []VariantDimension{VariantArchitecture, VariantFlavor}
}

// DefinitionOptions are the optional parameters to load.PackageDefinition
type DefinitionOptions struct {
Flavor string
Expand All @@ -41,6 +57,10 @@ type DefinitionOptions struct {
IsInteractive bool
// SkipVersionCheck skips version requirement validation
SkipVersionCheck bool
// SkipVariantFilters lists dimensions to retain instead of filtering out.
// e.g. VariantArchitecture retains every only.cluster.architecture variant of a
// component, as used when publishing skeletons.
SkipVariantFilters []VariantDimension
types.RemoteOptions
}

Expand Down Expand Up @@ -75,7 +95,11 @@ func PackageDefinition(ctx context.Context, packagePath string, opts DefinitionO
if err != nil {
return DefinedPackage{}, err
}
pkg.Metadata.Architecture = config.GetArch(pkg.Metadata.Architecture)
if slices.Contains(opts.SkipVariantFilters, VariantArchitecture) {
pkg.Metadata.Architecture = v1alpha1.SkeletonArch

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.

We still don't want the load package to know about skeletons, instead of setting the architecture, and checking for a skeleton architecture we should pass the variant dimensions into validate and import

} else {
pkg.Metadata.Architecture = config.GetArch(pkg.Metadata.Architecture)
}
opts.CachePath, err = utils.ResolveCachePath(opts.CachePath)
if err != nil {
return DefinedPackage{}, err
Expand Down
1 change: 1 addition & 0 deletions src/pkg/packager/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ func PublishSkeleton(ctx context.Context, path string, ref registry.Reference, o
Flavor: opts.Flavor,
SkipVersionCheck: opts.SkipVersionCheck,
SkipRequiredValues: true,
SkipVariantFilters: []load.VariantDimension{load.VariantArchitecture},
RemoteOptions: opts.RemoteOptions,
})
if err != nil {
Expand Down
25 changes: 25 additions & 0 deletions src/pkg/packager/publish_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,31 @@ func TestPublishSkeleton(t *testing.T) {
}
}

func TestPublishSkeletonMultiArch(t *testing.T) {
ctx := testutil.TestContext(t)
registryRef := createRegistry(ctx, t)

ref, err := PublishSkeleton(ctx, "testdata/skeleton-multiarch", registryRef, PublishSkeletonOptions{
RemoteOptions: defaultTestRemoteOptions(),
})
require.NoError(t, err)

rmt, err := zoci.NewRemote(ctx, ref.String(), zoci.PlatformForSkeleton(), oci.WithPlainHTTP(true))
require.NoError(t, err)
pkg, err := rmt.FetchZarfYAML(ctx)
require.NoError(t, err)

got := []string{}
for _, comp := range pkg.Components {
got = append(got, comp.Name+"/"+comp.Only.Cluster.Architecture)
}
// Skeletons retain every architecture variant for both direct and imported components.
require.ElementsMatch(t, []string{
"direct/amd64", "direct/arm64",
"imported/amd64", "imported/arm64",
}, got)
}

func TestPublishPackage(t *testing.T) {
signOpts := signing.DefaultSignBlobOptions()
signOpts.Key = filepath.Join("testdata", "publish", "cosign.key")
Expand Down
16 changes: 16 additions & 0 deletions src/pkg/packager/testdata/skeleton-multiarch/sub/zarf.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
kind: ZarfPackageConfig
metadata:
name: skeleton-multiarch-sub
description: sub-package with a same-named component gated per architecture

components:
- name: shared
required: true
only:
cluster:
architecture: amd64
- name: shared
required: true
only:
cluster:
architecture: arm64
22 changes: 22 additions & 0 deletions src/pkg/packager/testdata/skeleton-multiarch/zarf.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
kind: ZarfPackageConfig
metadata:
name: skeleton-multiarch
description: package with same-named components gated per architecture, direct and imported
version: 0.0.1

components:
- name: direct
required: true
only:
cluster:
architecture: amd64
- name: direct
required: true
only:
cluster:
architecture: arm64
- name: imported
required: true
import:
path: sub
name: shared
48 changes: 22 additions & 26 deletions src/test/e2e/10_component_flavor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,34 +85,30 @@ func TestPublishFlavor(t *testing.T) {
reg.Registry = testutil.SetupInMemoryRegistry(testutil.TestContext(t), t, 31888)

ref := reg.String()
expectedIDs := []string{"combined", "via-import"}

flavorTest := filepath.Join("src", "test", "packages", "10-package-flavors")
_, _, err := e2e.Zarf(t, "package", "publish", flavorTest, "--flavor", "vanilla", "--no-color", "oci://"+ref, "--plain-http")
require.NoError(t, err)

stdOut, _, err := e2e.Zarf(t, "package", "inspect", "definition", "oci://"+ref+"/test-package-flavors:v0.0.0-vanilla", "--plain-http", "-a", "skeleton")
require.NoError(t, err)

var config v1alpha1.ZarfPackage
err = yaml.Unmarshal([]byte(stdOut), &config)
require.NoError(t, err)

for i, component := range config.Components {
require.Equal(t, expectedIDs[i], component.Name)
// A skeleton is architecture-agnostic, so it retains every arch variant of the flavor's components.
assertSkeleton := func(t *testing.T, flavor string) {
t.Helper()
_, _, err := e2e.Zarf(t, "package", "publish", flavorTest, "--flavor", flavor, "oci://"+ref, "--plain-http")
require.NoError(t, err)

stdOut, _, err := e2e.Zarf(t, "package", "inspect", "definition", "oci://"+ref+"/test-package-flavors:v0.0.0-"+flavor, "--plain-http", "-a", "skeleton")
require.NoError(t, err)

var config v1alpha1.ZarfPackage
require.NoError(t, yaml.Unmarshal([]byte(stdOut), &config))

got := []string{}
for _, component := range config.Components {
got = append(got, component.Name+"/"+component.Only.Cluster.Architecture)
}
require.ElementsMatch(t, []string{
"combined/amd64", "combined/arm64",
"via-import/amd64", "via-import/arm64",
}, got)
}

flavorTest = filepath.Join("src", "test", "packages", "10-package-flavors")
_, _, err = e2e.Zarf(t, "package", "publish", flavorTest, "--flavor", "chocolate", "--no-color", "oci://"+ref, "--plain-http")
require.NoError(t, err)

stdOut, _, err = e2e.Zarf(t, "package", "inspect", "definition", "oci://"+ref+"/test-package-flavors:v0.0.0-chocolate", "--plain-http", "-a", "skeleton")
require.NoError(t, err)

err = yaml.Unmarshal([]byte(stdOut), &config)
require.NoError(t, err)

for i, component := range config.Components {
require.Equal(t, expectedIDs[i], component.Name)
}
assertSkeleton(t, "vanilla")
assertSkeleton(t, "chocolate")
}