Skip to content

Commit

Permalink
feat: Add backwards compatibility validation to package deploy (#1909)
Browse files Browse the repository at this point in the history
## Description

- Adds the latest version of Zarf with no breaking package structure
changes to build metadata in packages on `package create`

- Validates the running Zarf CLI version is not less (older) than the
last non-breaking version in a package on `package deploy`. Zarf will
throw a warning to the user if the CLI version is older than the last
non-breaking version to warn before deploying packages that are
potentially affected by breaking changes.

## Related Issue

Fixes #1760


## 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

- [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 authored Aug 2, 2023
1 parent 910846e commit b6611b3
Show file tree
Hide file tree
Showing 10 changed files with 210 additions and 19 deletions.
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 @@ -535,6 +535,22 @@ must respect the following conditions
</blockquote>
</details>

<details>
<summary>
<strong> <a name="build_lastNonBreakingVersion"></a>lastNonBreakingVersion</strong>
</summary>
&nbsp;
<blockquote>

**Description:** The minimum version of Zarf that does not have breaking package structure changes

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

</blockquote>
</details>

</blockquote>
</details>

Expand Down
19 changes: 10 additions & 9 deletions src/config/lang/english.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,15 +236,16 @@ const (
CmdPackageCreateCleanPathErr = "Invalid characters in Zarf cache path, defaulting to %s"
CmdPackageCreateErr = "Failed to create package: %s"

CmdPackageDeployFlagConfirm = "Confirms package deployment without prompting. ONLY use with packages you trust. Skips prompts to review SBOM, configure variables, select optional components and review potential breaking changes."
CmdPackageDeployFlagAdoptExistingResources = "Adopts any pre-existing K8s resources into the Helm charts managed by Zarf. ONLY use when you have existing deployments you want Zarf to takeover."
CmdPackageDeployFlagSet = "Specify deployment variables to set on the command line (KEY=value)"
CmdPackageDeployFlagComponents = "Comma-separated list of components to install. Adding this flag will skip the init prompts for which components to install"
CmdPackageDeployFlagShasum = "Shasum of the package to deploy. Required if deploying a remote package and \"--insecure\" is not provided"
CmdPackageDeployFlagSget = "[Deprecated] Path to public sget key file for remote packages signed via cosign. This flag will be removed in v0.31.0 please use the --key flag instead."
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"
CmdPackageDeployErr = "Failed to deploy package: %s"
CmdPackageDeployFlagConfirm = "Confirms package deployment without prompting. ONLY use with packages you trust. Skips prompts to review SBOM, configure variables, select optional components and review potential breaking changes."
CmdPackageDeployFlagAdoptExistingResources = "Adopts any pre-existing K8s resources into the Helm charts managed by Zarf. ONLY use when you have existing deployments you want Zarf to takeover."
CmdPackageDeployFlagSet = "Specify deployment variables to set on the command line (KEY=value)"
CmdPackageDeployFlagComponents = "Comma-separated list of components to install. Adding this flag will skip the init prompts for which components to install"
CmdPackageDeployFlagShasum = "Shasum of the package to deploy. Required if deploying a remote package and \"--insecure\" is not provided"
CmdPackageDeployFlagSget = "[Deprecated] Path to public sget key file for remote packages signed via cosign. This flag will be removed in v0.31.0 please use the --key flag instead."
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"
CmdPackageDeployValidateLastNonBreakingVersionWarn = "the version of this Zarf binary '%s' is less than the LastNonBreakingVersion of '%s'. You may need to upgrade your Zarf version to at least '%s' to deploy this package"
CmdPackageDeployErr = "Failed to deploy package: %s"

CmdPackageInspectFlagSbom = "View SBOM contents while inspecting the package"
CmdPackageInspectFlagSbomOut = "Specify an output directory for the SBOMs from the inspected Zarf package"
Expand Down
35 changes: 35 additions & 0 deletions src/pkg/packager/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"strings"

"github.com/AlecAivazis/survey/v2"
"github.com/Masterminds/semver/v3"
"github.com/defenseunicorns/zarf/src/config/lang"
"github.com/defenseunicorns/zarf/src/internal/cluster"
"github.com/defenseunicorns/zarf/src/internal/packager/sbom"
Expand Down Expand Up @@ -456,6 +457,40 @@ func (p *Packager) validatePackageArchitecture() error {
return nil
}

// validateLastNonBreakingVersion compares the Zarf CLI version against a package's LastNonBreakingVersion.
// It will return an error if there is an error parsing either of the two versions,
// and will throw a warning if the CLI version is less than the LastNonBreakingVersion.
func (p *Packager) validateLastNonBreakingVersion() (err error) {
cliVersion := config.CLIVersion
lastNonBreakingVersion := p.cfg.Pkg.Build.LastNonBreakingVersion

if lastNonBreakingVersion == "" || cliVersion == "UnknownVersion" {
return nil
}

lastNonBreakingSemVer, err := semver.NewVersion(lastNonBreakingVersion)
if err != nil {
return fmt.Errorf("unable to parse lastNonBreakingVersion '%s' from Zarf package build data : %w", lastNonBreakingVersion, err)
}

cliSemVer, err := semver.NewVersion(cliVersion)
if err != nil {
return fmt.Errorf("unable to parse Zarf CLI version '%s' : %w", cliVersion, err)
}

if cliSemVer.LessThan(lastNonBreakingSemVer) {
warning := fmt.Sprintf(
lang.CmdPackageDeployValidateLastNonBreakingVersionWarn,
cliVersion,
lastNonBreakingVersion,
lastNonBreakingVersion,
)
p.warnings = append(p.warnings, warning)
}

return nil
}

var (
// ErrPkgKeyButNoSig is returned when a key was provided but the package is not signed
ErrPkgKeyButNoSig = errors.New("a key was provided but the package is not signed - remove the --key flag and run the command again")
Expand Down
120 changes: 120 additions & 0 deletions src/pkg/packager/common_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package packager

import (
"fmt"
"testing"

"github.com/defenseunicorns/zarf/src/config"
"github.com/defenseunicorns/zarf/src/config/lang"
"github.com/defenseunicorns/zarf/src/types"
"github.com/stretchr/testify/assert"
)

// TestValidateLastNonBreakingVersion verifies that Zarf validates the lastNonBreakingVersion of packages against the CLI version correctly.
func TestValidateLastNonBreakingVersion(t *testing.T) {
t.Parallel()

type testCase struct {
name string
cliVersion string
lastNonBreakingVersion string
expectedErrorMessage string
expectedWarningMessage string
returnError bool
throwWarning bool
}

testCases := []testCase{
{
name: "CLI version less than lastNonBreakingVersion",
cliVersion: "v0.26.4",
lastNonBreakingVersion: "v0.27.0",
returnError: false,
throwWarning: true,
expectedWarningMessage: fmt.Sprintf(
lang.CmdPackageDeployValidateLastNonBreakingVersionWarn,
"v0.26.4",
"v0.27.0",
"v0.27.0",
),
},
{
name: "invalid semantic version (CLI version)",
cliVersion: "invalidSemanticVersion",
lastNonBreakingVersion: "v0.0.1",
throwWarning: false,
returnError: true,
expectedErrorMessage: "unable to parse Zarf CLI version",
},
{
name: "invalid semantic version (lastNonBreakingVersion)",
cliVersion: "v0.0.1",
lastNonBreakingVersion: "invalidSemanticVersion",
throwWarning: false,
returnError: true,
expectedErrorMessage: "unable to parse lastNonBreakingVersion",
},
{
name: "CLI version greater than lastNonBreakingVersion",
cliVersion: "v0.28.2",
lastNonBreakingVersion: "v0.27.0",
returnError: false,
throwWarning: false,
},
{
name: "CLI version equal to lastNonBreakingVersion",
cliVersion: "v0.27.0",
lastNonBreakingVersion: "v0.27.0",
returnError: false,
throwWarning: false,
},
{
name: "empty lastNonBreakingVersion",
cliVersion: "this shouldn't get evaluated when the lastNonBreakingVersion is empty",
lastNonBreakingVersion: "",
returnError: false,
throwWarning: false,
},
{
name: "default CLI version in E2E tests",
cliVersion: "UnknownVersion", // This is used as a default version in the E2E tests
lastNonBreakingVersion: "v0.27.0",
returnError: false,
throwWarning: false,
},
}

for _, testCase := range testCases {
testCase := testCase

t.Run(testCase.name, func(t *testing.T) {
t.Parallel()

config.CLIVersion = testCase.cliVersion

p := &Packager{
cfg: &types.PackagerConfig{
Pkg: types.ZarfPackage{
Build: types.ZarfBuildData{
LastNonBreakingVersion: testCase.lastNonBreakingVersion,
},
},
},
}

err := p.validateLastNonBreakingVersion()

switch {
case testCase.returnError:
assert.ErrorContains(t, err, testCase.expectedErrorMessage)
assert.Empty(t, p.warnings, "Expected no warnings for test case: %s", testCase.name)
case testCase.throwWarning:
assert.Contains(t, p.warnings, testCase.expectedWarningMessage)
assert.NoError(t, err, "Expected no error for test case: %s", testCase.name)
default:
assert.NoError(t, err, "Expected no error for test case: %s", testCase.name)
assert.Empty(t, p.warnings, "Expected no warnings for test case: %s", testCase.name)
}
})
}
}
4 changes: 4 additions & 0 deletions src/pkg/packager/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ func (p *Packager) Deploy() error {
return err
}

if err := p.validateLastNonBreakingVersion(); err != nil {
return err
}

// Now that we have read the zarf.yaml, check the package kind
if p.cfg.Pkg.Kind == "ZarfInitConfig" {
p.cfg.IsInitConfig = true
Expand Down
2 changes: 2 additions & 0 deletions src/pkg/packager/deprecated/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ type BreakingChange struct {

// List of migrations tracked in the zarf.yaml build data.
const (
// This should be updated when a breaking change is introduced to the Zarf package structure. See: https://github.com/defenseunicorns/zarf/releases/tag/v0.27.0
LastNonBreakingVersion = "v0.27.0"
ScriptsToActionsMigrated = "scripts-to-actions"
PluralizeSetVariable = "pluralize-set-variable"
)
Expand Down
3 changes: 3 additions & 0 deletions src/pkg/packager/yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,5 +79,8 @@ func (p *Packager) writeYaml() error {

p.cfg.Pkg.Build.RegistryOverrides = p.cfg.CreateOpts.RegistryOverrides

// Record the latest version of Zarf without breaking changes to the package structure.
p.cfg.Pkg.Build.LastNonBreakingVersion = deprecated.LastNonBreakingVersion

return utils.WriteYaml(p.tmp.ZarfYaml, p.cfg.Pkg, 0400)
}
21 changes: 11 additions & 10 deletions src/types/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,17 @@ type ZarfMetadata struct {

// ZarfBuildData is written during the packager.Create() operation to track details of the created package.
type ZarfBuildData struct {
Terminal string `json:"terminal" jsonschema:"description=The machine name that created this package"`
User string `json:"user" jsonschema:"description=The username who created this package"`
Architecture string `json:"architecture" jsonschema:"description=The architecture this package was created on"`
Timestamp string `json:"timestamp" jsonschema:"description=The timestamp when this package was created"`
Version string `json:"version" jsonschema:"description=The version of Zarf used to build this package"`
Migrations []string `json:"migrations" jsonschema:"description=Any migrations that have been run on this package"`
Differential bool `json:"differential" jsonschema:"description=Whether this package was created with differential components"`
RegistryOverrides map[string]string `json:"registryOverrides" jsonschema:"description=Any registry domains that were overridden on package create when pulling images"`
DifferentialMissing []string `json:"differentialMissing,omitempty" jsonschema:"description=List of components that were not included in this package due to differential packaging"`
OCIImportedComponents map[string]string `json:"OCIImportedComponents,omitempty" jsonschema:"description=Map of components that were imported via OCI. The keys are OCI Package URLs and values are the component names"`
Terminal string `json:"terminal" jsonschema:"description=The machine name that created this package"`
User string `json:"user" jsonschema:"description=The username who created this package"`
Architecture string `json:"architecture" jsonschema:"description=The architecture this package was created on"`
Timestamp string `json:"timestamp" jsonschema:"description=The timestamp when this package was created"`
Version string `json:"version" jsonschema:"description=The version of Zarf used to build this package"`
Migrations []string `json:"migrations" jsonschema:"description=Any migrations that have been run on this package"`
Differential bool `json:"differential" jsonschema:"description=Whether this package was created with differential components"`
RegistryOverrides map[string]string `json:"registryOverrides" jsonschema:"description=Any registry domains that were overridden on package create when pulling images"`
DifferentialMissing []string `json:"differentialMissing,omitempty" jsonschema:"description=List of components that were not included in this package due to differential packaging"`
OCIImportedComponents map[string]string `json:"OCIImportedComponents,omitempty" jsonschema:"description=Map of components that were imported via OCI. The keys are OCI Package URLs and values are the component names"`
LastNonBreakingVersion string `json:"lastNonBreakingVersion,omitempty" jsonschema:"description=The minimum version of Zarf that does not have breaking package structure changes"`
}

// ZarfPackageVariable are variables that can be used to dynamically template K8s resources.
Expand Down
5 changes: 5 additions & 0 deletions src/ui/lib/api-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,10 @@ export interface ZarfBuildData {
* List of components that were not included in this package due to differential packaging
*/
differentialMissing?: string[];
/**
* The minimum version of Zarf that does not have breaking package structure changes
*/
lastNonBreakingVersion?: string;
/**
* Any migrations that have been run on this package
*/
Expand Down Expand Up @@ -1469,6 +1473,7 @@ const typeMap: any = {
{ json: "architecture", js: "architecture", typ: "" },
{ json: "differential", js: "differential", typ: true },
{ json: "differentialMissing", js: "differentialMissing", typ: u(undefined, a("")) },
{ json: "lastNonBreakingVersion", js: "lastNonBreakingVersion", typ: u(undefined, "") },
{ json: "migrations", js: "migrations", typ: a("") },
{ json: "OCIImportedComponents", js: "OCIImportedComponents", typ: u(undefined, m("")) },
{ json: "registryOverrides", js: "registryOverrides", typ: m("") },
Expand Down
4 changes: 4 additions & 0 deletions zarf.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@
},
"type": "object",
"description": "Map of components that were imported via OCI. The keys are OCI Package URLs and values are the component names"
},
"lastNonBreakingVersion": {
"type": "string",
"description": "The minimum version of Zarf that does not have breaking package structure changes"
}
},
"additionalProperties": false,
Expand Down

0 comments on commit b6611b3

Please sign in to comment.