diff --git a/docs/2-the-zarf-cli/100-cli-commands/zarf_package_create.md b/docs/2-the-zarf-cli/100-cli-commands/zarf_package_create.md index 8bd70e500d..79f8fbe7f7 100644 --- a/docs/2-the-zarf-cli/100-cli-commands/zarf_package_create.md +++ b/docs/2-the-zarf-cli/100-cli-commands/zarf_package_create.md @@ -18,6 +18,7 @@ zarf package create [ DIRECTORY ] [flags] ``` --confirm Confirm package creation without prompting --differential string [beta] Build a package that only contains the differential changes from local resources and differing remote resources from the specified previously built package + -f, --flavor string The flavor of components to include in the resulting package (i.e. have a matching or empty "only.flavor" key) -h, --help help for create -m, --max-package-size int Specify the maximum size of the package in megabytes, packages larger than this will be split into multiple parts to be loaded onto smaller media (i.e. DVDs). Use 0 to disable splitting. -o, --output string Specify the output (either a directory or an oci:// URL) for the created Zarf package diff --git a/docs/3-create-a-zarf-package/4-zarf-schema.md b/docs/3-create-a-zarf-package/4-zarf-schema.md index 478a863c26..241382d6f6 100644 --- a/docs/3-create-a-zarf-package/4-zarf-schema.md +++ b/docs/3-create-a-zarf-package/4-zarf-schema.md @@ -718,6 +718,22 @@ Must be one of: +
+ + flavor + +  +
+ +**Description:** Only include this component when a matching '--flavor' is specified on 'zarf package create' + +| | | +| -------- | -------- | +| **Type** | `string` | + +
+
+ diff --git a/examples/component-choice/README.md b/examples/component-choice/README.md index 1d5a2356a6..81a57fb6cf 100644 --- a/examples/component-choice/README.md +++ b/examples/component-choice/README.md @@ -4,7 +4,7 @@ import ExampleYAML from "@site/src/components/ExampleYAML"; :::caution -Component Choice is currently a [Deprecated Feature](../../docs/9-roadmap.md#alpha). This feature will be removed in Zarf v1.0.0. Please migrate any existing packages you may have that utilize it. +Component Choice is currently a [Deprecated Feature](../../docs/9-roadmap.md#alpha). This feature will be removed in Zarf v1.0.0. Please migrate any existing packages you may have that utilize it. In doing so you may want to consider [Package Flavors](../package-flavors/README.md) as an alternative. ::: diff --git a/examples/package-flavors/README.md b/examples/package-flavors/README.md new file mode 100644 index 0000000000..300d4dbbfc --- /dev/null +++ b/examples/package-flavors/README.md @@ -0,0 +1,17 @@ +import ExampleYAML from "@site/src/components/ExampleYAML"; + +# Package Flavors + +This example demonstrates how to define variants of packages within the same package definition. This can be combined with [Composable Packages](../composable-packages/README.md) to build up packages and include the necessary [merge overrides](../composable-packages/README.md#merge-strategies) for each variant. + +Given package flavors are built by specifying the `--flavor` flag on `zarf package create`. This will include any components that match that flavor or that do not specify a flavor. + +## `zarf.yaml` {#zarf.yaml} + +:::info + +To view the example in its entirety, select the `Edit this page` link below the article and select the parent folder. + +::: + + diff --git a/examples/package-flavors/pod.yaml b/examples/package-flavors/pod.yaml new file mode 100644 index 0000000000..e7211dcd6d --- /dev/null +++ b/examples/package-flavors/pod.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Pod +metadata: + name: enterprise-linux + labels: + app: enterprise-linux +spec: + containers: + - name: enterprise-linux-container + image: "###ZARF_VAR_IMAGE###" + command: [ "sh", "-c", "while true; do ls; sleep 1; done"] + resources: + requests: + memory: "32Mi" + cpu: "50m" + limits: + memory: "128Mi" + cpu: "250m" diff --git a/examples/package-flavors/zarf.yaml b/examples/package-flavors/zarf.yaml new file mode 100644 index 0000000000..8650ce69e1 --- /dev/null +++ b/examples/package-flavors/zarf.yaml @@ -0,0 +1,70 @@ +kind: ZarfPackageConfig +metadata: + name: package-flavors + description: Simple example to show how to use the `only.flavor` key to build package variants. + +components: + - name: image + required: true + description: "Sets the Enterprise Linux flavor to Rocky Linux" + only: + flavor: rocky-road + images: + - rockylinux:9-minimal + actions: + onDeploy: + before: + - cmd: echo "rockylinux:9-minimal" + setVariables: + - name: IMAGE + + - name: image + required: true + description: "Sets the Enterprise Linux flavor to Oracle Linux" + only: + flavor: oracle-cookie-crunch + images: + - oraclelinux:9-slim + actions: + onDeploy: + before: + - cmd: echo "oraclelinux:9-slim" + setVariables: + - name: IMAGE + + - name: image + required: true + description: "Sets the Enterprise Linux flavor to Alma Linux" + only: + flavor: vanilla-alma-nd + images: + - almalinux:9-minimal + actions: + onDeploy: + before: + - cmd: echo "almalinux:9-minimal" + setVariables: + - name: IMAGE + + - name: image + required: true + description: "Sets the Enterprise Linux flavor to OpenSUSE" + only: + flavor: strawberry-suse + images: + - opensuse/leap:15 + actions: + onDeploy: + before: + - cmd: echo "opensuse/leap:15" + setVariables: + - name: IMAGE + + - name: pod + description: "The pod that runs the specified flavor of Enterprise Linux" + required: true + manifests: + - name: enterprise-linux + namespace: enterprise-linux + files: + - pod.yaml diff --git a/src/cmd/common/viper.go b/src/cmd/common/viper.go index 921458235d..f6185139d3 100644 --- a/src/cmd/common/viper.go +++ b/src/cmd/common/viper.go @@ -73,6 +73,7 @@ const ( VPkgCreateSigningKeyPassword = "package.create.signing_key_password" VPkgCreateDifferential = "package.create.differential" VPkgCreateRegistryOverride = "package.create.registry_override" + VPkgCreateFlavor = "package.create.flavor" // Package deploy config keys diff --git a/src/cmd/package.go b/src/cmd/package.go index 14eb104ab2..3afb9aba35 100644 --- a/src/cmd/package.go +++ b/src/cmd/package.go @@ -350,6 +350,7 @@ func bindCreateFlags(v *viper.Viper) { createFlags.BoolVar(&pkgConfig.CreateOpts.SkipSBOM, "skip-sbom", v.GetBool(common.VPkgCreateSkipSbom), lang.CmdPackageCreateFlagSkipSbom) createFlags.IntVarP(&pkgConfig.CreateOpts.MaxPackageSizeMB, "max-package-size", "m", v.GetInt(common.VPkgCreateMaxPackageSize), lang.CmdPackageCreateFlagMaxPackageSize) createFlags.StringToStringVar(&pkgConfig.CreateOpts.RegistryOverrides, "registry-override", v.GetStringMapString(common.VPkgCreateRegistryOverride), lang.CmdPackageCreateFlagRegistryOverride) + createFlags.StringVarP(&pkgConfig.CreateOpts.Flavor, "flavor", "f", v.GetString(common.VPkgCreateFlavor), lang.CmdPackageCreateFlagFlavor) createFlags.StringVar(&pkgConfig.CreateOpts.SigningKeyPath, "signing-key", v.GetString(common.VPkgCreateSigningKey), lang.CmdPackageCreateFlagSigningKey) createFlags.StringVar(&pkgConfig.CreateOpts.SigningKeyPassword, "signing-key-pass", v.GetString(common.VPkgCreateSigningKeyPassword), lang.CmdPackageCreateFlagSigningKeyPassword) diff --git a/src/config/lang/english.go b/src/config/lang/english.go index 94380e542f..7c4b21c94f 100644 --- a/src/config/lang/english.go +++ b/src/config/lang/english.go @@ -251,6 +251,7 @@ const ( CmdPackageCreateFlagDeprecatedKeyPassword = "[Deprecated] Password to the private key file used for signing packages (use --signing-key-pass instead)" CmdPackageCreateFlagDifferential = "[beta] Build a package that only contains the differential changes from local resources and differing remote resources from the specified previously built package" CmdPackageCreateFlagRegistryOverride = "Specify a map of domains to override on package create when pulling images (e.g. --registry-override docker.io=dockerio-reg.enterprise.intranet)" + CmdPackageCreateFlagFlavor = "The flavor of components to include in the resulting package (i.e. have a matching or empty \"only.flavor\" key)" CmdPackageCreateCleanPathErr = "Invalid characters in Zarf cache path, defaulting to %s" CmdPackageCreateErr = "Failed to create package: %s" diff --git a/src/pkg/packager/compose.go b/src/pkg/packager/compose.go index b390dfb5f4..1f1c94e471 100644 --- a/src/pkg/packager/compose.go +++ b/src/pkg/packager/compose.go @@ -20,12 +20,16 @@ func (p *Packager) composeComponents() error { for _, component := range p.cfg.Pkg.Components { arch := p.arch // filter by architecture - if component.Only.Cluster.Architecture != "" && component.Only.Cluster.Architecture != arch { + if !composer.CompatibleComponent(component, arch, p.cfg.CreateOpts.Flavor) { continue + } else { + // if a match was found, strip flavor and architecture to reduce bloat in the package definition + component.Only.Cluster.Architecture = "" + component.Only.Flavor = "" } // build the import chain - chain, err := composer.NewImportChain(component, arch) + chain, err := composer.NewImportChain(component, arch, p.cfg.CreateOpts.Flavor) if err != nil { return err } diff --git a/src/pkg/packager/composer/list.go b/src/pkg/packager/composer/list.go index 0db1aaffb0..8354bb958f 100644 --- a/src/pkg/packager/composer/list.go +++ b/src/pkg/packager/composer/list.go @@ -72,7 +72,7 @@ func (ic *ImportChain) append(c types.ZarfComponent, relativeToHead string, vars } // NewImportChain creates a new import chain from a component -func NewImportChain(head types.ZarfComponent, arch string) (*ImportChain, error) { +func NewImportChain(head types.ZarfComponent, arch, flavor string) (*ImportChain, error) { if arch == "" { return nil, fmt.Errorf("cannot build import chain: architecture must be provided") } @@ -143,8 +143,7 @@ func NewImportChain(head types.ZarfComponent, arch string) (*ImportChain, error) found := helpers.Filter(pkg.Components, func(c types.ZarfComponent) bool { matchesName := c.Name == name - satisfiesArch := c.Only.Cluster.Architecture == "" || c.Only.Cluster.Architecture == arch - return matchesName && satisfiesArch + return matchesName && CompatibleComponent(c, arch, flavor) }) if len(found) == 0 { @@ -282,3 +281,10 @@ func (ic *ImportChain) MergeConstants(existing []types.ZarfPackageConstant) (mer } return merged } + +// CompatibleComponent determines if this component is compatible with the given create options +func CompatibleComponent(c types.ZarfComponent, arch, flavor string) bool { + satisfiesArch := c.Only.Cluster.Architecture == "" || c.Only.Cluster.Architecture == arch + satisfiesFlavor := c.Only.Flavor == "" || c.Only.Flavor == flavor + return satisfiesArch && satisfiesFlavor +} diff --git a/src/pkg/packager/composer/list_test.go b/src/pkg/packager/composer/list_test.go index 6d250f6e44..abcce2b8df 100644 --- a/src/pkg/packager/composer/list_test.go +++ b/src/pkg/packager/composer/list_test.go @@ -22,6 +22,7 @@ func TestNewImportChain(t *testing.T) { name string head types.ZarfComponent arch string + flavor string expectedErrorMessage string } @@ -49,7 +50,7 @@ func TestNewImportChain(t *testing.T) { t.Run(testCase.name, func(t *testing.T) { t.Parallel() - _, err := NewImportChain(testCase.head, testCase.arch) + _, err := NewImportChain(testCase.head, testCase.arch, testCase.flavor) require.Contains(t, err.Error(), testCase.expectedErrorMessage) }) } diff --git a/src/pkg/packager/deprecated/common.go b/src/pkg/packager/deprecated/common.go index 2b036585ee..2fdbfdcaf3 100644 --- a/src/pkg/packager/deprecated/common.go +++ b/src/pkg/packager/deprecated/common.go @@ -81,7 +81,7 @@ func MigrateComponent(build types.ZarfBuildData, component types.ZarfComponent) func PrintBreakingChanges(deployedZarfVersion string) { deployedSemver, err := semver.NewVersion(deployedZarfVersion) if err != nil { - message.Warnf("Unable to determine init-package version from %s. There is potential for breaking changes.", deployedZarfVersion) + message.Debugf("Unable to check for breaking changes between Zarf versions") return } diff --git a/src/test/e2e/09_component_compose_test.go b/src/test/e2e/09_component_compose_test.go index 7afb5ac0c3..a244b5d9c1 100644 --- a/src/test/e2e/09_component_compose_test.go +++ b/src/test/e2e/09_component_compose_test.go @@ -45,7 +45,7 @@ func (suite *CompositionSuite) TearDownSuite() { func (suite *CompositionSuite) Test_0_ComposabilityExample() { suite.T().Log("E2E: Package Compose Example") - _, stdErr, err := e2e.Zarf("package", "create", composeExample, "-o", "build", "--insecure", "--no-color", "--confirm") + _, stdErr, err := e2e.Zarf("package", "create", composeExample, "-o", "build", "--no-color", "--confirm") suite.NoError(err) // Ensure that common names merge @@ -70,7 +70,7 @@ func (suite *CompositionSuite) Test_0_ComposabilityExample() { func (suite *CompositionSuite) Test_1_FullComposability() { suite.T().Log("E2E: Full Package Compose") - _, stdErr, err := e2e.Zarf("package", "create", composeTest, "-o", "build", "--insecure", "--no-color", "--confirm") + _, stdErr, err := e2e.Zarf("package", "create", composeTest, "-o", "build", "--no-color", "--confirm") suite.NoError(err) // Ensure that names merge and that composition is added appropriately diff --git a/src/test/e2e/10_component_flavor_test.go b/src/test/e2e/10_component_flavor_test.go new file mode 100644 index 0000000000..5387491312 --- /dev/null +++ b/src/test/e2e/10_component_flavor_test.go @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package test provides e2e tests for Zarf. +package test + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +type FlavorSuite struct { + suite.Suite + *require.Assertions +} + +var ( + flavorExample = filepath.Join("examples", "package-flavors") + flavorTest = filepath.Join("src", "test", "packages", "10-package-flavors") + flavorExamplePath string + flavorTestAMDPath = filepath.Join("build", "zarf-package-test-package-flavors-amd64.tar.zst") + flavorTestARMPath = filepath.Join("build", "zarf-package-test-package-flavors-arm64.tar.zst") +) + +func (suite *FlavorSuite) SetupSuite() { + suite.Assertions = require.New(suite.T()) + + // Setup the example package path after e2e has been initialized + flavorExamplePath = filepath.Join("build", fmt.Sprintf("zarf-package-package-flavors-%s.tar.zst", e2e.Arch)) +} + +func (suite *FlavorSuite) TearDownSuite() { + err := os.RemoveAll(flavorExamplePath) + suite.NoError(err) + err = os.RemoveAll(flavorTestAMDPath) + suite.NoError(err) + err = os.RemoveAll(flavorTestARMPath) + suite.NoError(err) +} + +func (suite *FlavorSuite) Test_0_FlavorExample() { + suite.T().Log("E2E: Package Flavor Example") + + _, stdErr, err := e2e.Zarf("package", "create", flavorExample, "-o", "build", "--flavor", "oracle-cookie-crunch", "--no-color", "--confirm") + suite.NoError(err) + + // Ensure that the oracle image is included + suite.Contains(stdErr, `oraclelinux:9-slim`) + + // Ensure that the common pod was included + suite.Contains(stdErr, `description: The pod that runs the specified flavor of Enterprise Linux`) + + // Ensure that the other flavors are not included + suite.NotContains(stdErr, `rockylinux:9-minimal`) + suite.NotContains(stdErr, `almalinux:9-minimal`) + suite.NotContains(stdErr, `opensuse/leap:15`) +} + +func (suite *FlavorSuite) Test_1_FlavorArchFiltering() { + suite.T().Log("E2E: Package Flavor + Arch Filtering") + + _, stdErr, err := e2e.Zarf("package", "create", flavorTest, "-o", "build", "--flavor", "vanilla", "-a", "amd64", "--no-color", "--confirm") + suite.NoError(err) + + // Ensure that the initial filter was applied + suite.Contains(stdErr, ` +- name: combined + description: vanilla-amd`) + + // Ensure that the import filter was applied + suite.Contains(stdErr, ` +- name: via-import + description: vanilla-amd`) + + // Ensure that the other flavors / architectures are not included + suite.NotContains(stdErr, `vanilla-arm`) + suite.NotContains(stdErr, `chocolate-amd`) + suite.NotContains(stdErr, `chocolate-arm`) + + _, stdErr, err = e2e.Zarf("package", "create", flavorTest, "-o", "build", "--flavor", "chocolate", "-a", "amd64", "--no-color", "--confirm") + suite.NoError(err) + + // Ensure that the initial filter was applied + suite.Contains(stdErr, ` +- name: combined + description: chocolate-amd`) + + // Ensure that the import filter was applied + suite.Contains(stdErr, ` +- name: via-import + description: chocolate-amd`) + + // Ensure that the other flavors / architectures are not included + suite.NotContains(stdErr, `vanilla-arm`) + suite.NotContains(stdErr, `vanilla-amd`) + suite.NotContains(stdErr, `chocolate-arm`) + + _, stdErr, err = e2e.Zarf("package", "create", flavorTest, "-o", "build", "--flavor", "chocolate", "-a", "arm64", "--no-color", "--confirm") + suite.NoError(err) + + // Ensure that the initial filter was applied + suite.Contains(stdErr, ` +- name: combined + description: chocolate-arm`) + + // Ensure that the import filter was applied + suite.Contains(stdErr, ` +- name: via-import + description: chocolate-arm`) + + // Ensure that the other flavors / architectures are not included + suite.NotContains(stdErr, `vanilla-arm`) + suite.NotContains(stdErr, `vanilla-amd`) + suite.NotContains(stdErr, `chocolate-amd`) +} + +func TestFlavorSuite(t *testing.T) { + e2e.SetupWithCluster(t) + + suite.Run(t, new(FlavorSuite)) +} diff --git a/src/test/packages/10-package-flavors/sub-package/zarf.yaml b/src/test/packages/10-package-flavors/sub-package/zarf.yaml new file mode 100644 index 0000000000..b6f2f87151 --- /dev/null +++ b/src/test/packages/10-package-flavors/sub-package/zarf.yaml @@ -0,0 +1,33 @@ +kind: ZarfPackageConfig +metadata: + name: test-sub-package-flavors + description: A contrived example for package flavor / arch filter testing + +components: + - name: combined + description: "vanilla-amd" + only: + cluster: + architecture: "amd64" + flavor: "vanilla" + + - name: combined + description: "vanilla-arm" + only: + cluster: + architecture: "arm64" + flavor: "vanilla" + + - name: combined + description: "chocolate-amd" + only: + cluster: + architecture: "amd64" + flavor: "chocolate" + + - name: combined + description: "chocolate-arm" + only: + cluster: + architecture: "arm64" + flavor: "chocolate" diff --git a/src/test/packages/10-package-flavors/zarf.yaml b/src/test/packages/10-package-flavors/zarf.yaml new file mode 100644 index 0000000000..0e4f3dcb99 --- /dev/null +++ b/src/test/packages/10-package-flavors/zarf.yaml @@ -0,0 +1,38 @@ +kind: ZarfPackageConfig +metadata: + name: test-package-flavors + description: A contrived example for package flavor / arch filter testing + +components: + - name: combined + description: "vanilla-amd" + only: + cluster: + architecture: "amd64" + flavor: "vanilla" + + - name: combined + description: "vanilla-arm" + only: + cluster: + architecture: "arm64" + flavor: "vanilla" + + - name: combined + description: "chocolate-amd" + only: + cluster: + architecture: "amd64" + flavor: "chocolate" + + - name: combined + description: "chocolate-arm" + only: + cluster: + architecture: "arm64" + flavor: "chocolate" + + - name: via-import + import: + path: sub-package + name: combined diff --git a/src/types/component.go b/src/types/component.go index df5f37c836..ae9c88b4b5 100644 --- a/src/types/component.go +++ b/src/types/component.go @@ -29,7 +29,7 @@ type ZarfComponent struct { // Key to match other components to produce a user selector field, used to create a BOOLEAN XOR for a set of components // Note: ignores default and required flags - Group string `json:"group,omitempty" jsonschema:"description=[Deprecated] Create a user selector field based on all components in the same group. This will be removed in Zarf v1.0.0.,deprecated=true"` + Group string `json:"group,omitempty" jsonschema:"description=[Deprecated] Create a user selector field based on all components in the same group. This will be removed in Zarf v1.0.0. Consider using 'only.flavor' instead.,deprecated=true"` // (Deprecated) Path to cosign public key for signed online resources DeprecatedCosignKeyPath string `json:"cosignKeyPath,omitempty" jsonschema:"description=[Deprecated] Specify a path to a public key to validate signed online resources. This will be removed in Zarf v1.0.0.,deprecated=true"` @@ -69,6 +69,7 @@ type ZarfComponent struct { type ZarfComponentOnlyTarget struct { LocalOS string `json:"localOS,omitempty" jsonschema:"description=Only deploy component to specified OS,enum=linux,enum=darwin,enum=windows"` Cluster ZarfComponentOnlyCluster `json:"cluster,omitempty" jsonschema:"description=Only deploy component to specified clusters"` + Flavor string `json:"flavor,omitempty" jsonschema:"description=Only include this component when a matching '--flavor' is specified on 'zarf package create'"` } // ZarfComponentOnlyCluster represents the architecture and K8s cluster distribution to filter on. diff --git a/src/types/runtime.go b/src/types/runtime.go index 2a29484f19..a812043a68 100644 --- a/src/types/runtime.go +++ b/src/types/runtime.go @@ -94,6 +94,7 @@ type ZarfCreateOptions struct { SigningKeyPassword string `json:"signingKeyPassword" jsonschema:"description=Password to the private key signature file that will be used to sigh the created package"` DifferentialData DifferentialData `json:"differential" jsonschema:"description=A package's differential images and git repositories from a referenced previously built package"` RegistryOverrides map[string]string `json:"registryOverrides" jsonschema:"description=A map of domains to override on package create when pulling images"` + Flavor string `json:"flavor" jsonschema:"description=An optional variant that controls which components will be included in a package"` } // ZarfSplitPackageData contains info about a split package. diff --git a/zarf.schema.json b/zarf.schema.json index f2ff08fd64..3491cda363 100644 --- a/zarf.schema.json +++ b/zarf.schema.json @@ -226,7 +226,7 @@ }, "group": { "type": "string", - "description": "[Deprecated] Create a user selector field based on all components in the same group. This will be removed in Zarf v1.0.0." + "description": "[Deprecated] Create a user selector field based on all components in the same group. This will be removed in Zarf v1.0.0. Consider using 'only.flavor' instead." }, "cosignKeyPath": { "type": "string", @@ -684,6 +684,10 @@ "$schema": "http://json-schema.org/draft-04/schema#", "$ref": "#/definitions/ZarfComponentOnlyCluster", "description": "Only deploy component to specified clusters" + }, + "flavor": { + "type": "string", + "description": "Only include this component when a matching '--flavor' is specified on 'zarf package create'" } }, "additionalProperties": false,