Skip to content

Commit

Permalink
feat: introduce the only.flavor key to allow building of package va…
Browse files Browse the repository at this point in the history
…riants (#2105)

## Description

This implements the `only.flavor` filter as a replacement for component
groups to allow the feature to be more declarative when building package
variants.

## Related Issue

Fixes #2101

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)
- [X] New feature (non-breaking change which adds functionality)
- [ ] Other (security config, docs update, etc)

## Checklist before merging

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

---------

Signed-off-by: razzle <[email protected]>
Co-authored-by: razzle <[email protected]>
  • Loading branch information
Racer159 and Noxsios authored Nov 1, 2023
1 parent ff70c6a commit e255baa
Show file tree
Hide file tree
Showing 20 changed files with 351 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions docs/3-create-a-zarf-package/4-zarf-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,22 @@ Must be one of:
</blockquote>
</details>

<details>
<summary>
<strong> <a name="components_items_only_flavor"></a>flavor</strong>
</summary>
&nbsp;
<blockquote>

**Description:** Only include this component when a matching '--flavor' is specified on 'zarf package create'

| | |
| -------- | -------- |
| **Type** | `string` |

</blockquote>
</details>

</blockquote>
</details>

Expand Down
2 changes: 1 addition & 1 deletion examples/component-choice/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

:::

Expand Down
17 changes: 17 additions & 0 deletions examples/package-flavors/README.md
Original file line number Diff line number Diff line change
@@ -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.

:::

<ExampleYAML src={require('./zarf.yaml')} showLink={false} />
18 changes: 18 additions & 0 deletions examples/package-flavors/pod.yaml
Original file line number Diff line number Diff line change
@@ -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"
70 changes: 70 additions & 0 deletions examples/package-flavors/zarf.yaml
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions src/cmd/common/viper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions src/cmd/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
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 @@ -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"

Expand Down
8 changes: 6 additions & 2 deletions src/pkg/packager/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
12 changes: 9 additions & 3 deletions src/pkg/packager/composer/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
3 changes: 2 additions & 1 deletion src/pkg/packager/composer/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func TestNewImportChain(t *testing.T) {
name string
head types.ZarfComponent
arch string
flavor string
expectedErrorMessage string
}

Expand Down Expand Up @@ -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)
})
}
Expand Down
2 changes: 1 addition & 1 deletion src/pkg/packager/deprecated/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
4 changes: 2 additions & 2 deletions src/test/e2e/09_component_compose_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
126 changes: 126 additions & 0 deletions src/test/e2e/10_component_flavor_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
Loading

0 comments on commit e255baa

Please sign in to comment.