Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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")
}
8 changes: 7 additions & 1 deletion src/pkg/packager/load/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ type DefinitionOptions struct {
IsInteractive bool
// SkipVersionCheck skips version requirement validation
SkipVersionCheck bool
// Skeleton retains all architecture variants instead of filtering to the build host arch
Skeleton bool

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.

I'd like to avoid tying load.PackageDefinition to skeletons.

I think something like below would be a better abstraction. It would also aid the work that @chaospuppy is doing in #4783.

type VariantDimension string

const (
    VariantArchitecture VariantDimension = "architecture"
    VariantFlavor       VariantDimension = "flavor"
)

// SkipVariantFilters lists dimensions to retain instead of filtering out.
SkipVariantFilters []VariantDimension

func AllVariantDimension() []VariantDimension

@JeffResc JeffResc Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just pushed a commit to address this, let me know any feedback you have

types.RemoteOptions
}

Expand Down Expand Up @@ -75,7 +77,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 opts.Skeleton {
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,
Skeleton: true,
Comment thread
JeffResc marked this conversation as resolved.
Outdated
RemoteOptions: opts.RemoteOptions,
})
if err != nil {
Expand Down
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, "--no-color", "oci://"+ref, "--plain-http")
Comment thread
JeffResc marked this conversation as resolved.
Outdated
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")
}