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 819951f08e..704a03cac6 100644 --- a/docs/3-create-a-zarf-package/4-zarf-schema.md +++ b/docs/3-create-a-zarf-package/4-zarf-schema.md @@ -535,6 +535,22 @@ must respect the following conditions +
+ + lastNonBreakingVersion + +  +
+ +**Description:** The minimum version of Zarf that does not have breaking package structure changes + +| | | +| -------- | -------- | +| **Type** | `string` | + +
+
+ diff --git a/src/config/lang/english.go b/src/config/lang/english.go index 5ab24d2c93..8bcda88363 100644 --- a/src/config/lang/english.go +++ b/src/config/lang/english.go @@ -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" diff --git a/src/pkg/packager/common.go b/src/pkg/packager/common.go index 78f005f5cf..ad8120bdf1 100644 --- a/src/pkg/packager/common.go +++ b/src/pkg/packager/common.go @@ -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" @@ -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") diff --git a/src/pkg/packager/common_test.go b/src/pkg/packager/common_test.go new file mode 100644 index 0000000000..66decdde60 --- /dev/null +++ b/src/pkg/packager/common_test.go @@ -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) + } + }) + } +} diff --git a/src/pkg/packager/deploy.go b/src/pkg/packager/deploy.go index 798ffffdbc..9443e92eb0 100644 --- a/src/pkg/packager/deploy.go +++ b/src/pkg/packager/deploy.go @@ -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 diff --git a/src/pkg/packager/deprecated/common.go b/src/pkg/packager/deprecated/common.go index f2a79822e1..bdd9c9b49e 100644 --- a/src/pkg/packager/deprecated/common.go +++ b/src/pkg/packager/deprecated/common.go @@ -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" ) diff --git a/src/pkg/packager/yaml.go b/src/pkg/packager/yaml.go index 5a1ff6ab61..1c257ab42a 100644 --- a/src/pkg/packager/yaml.go +++ b/src/pkg/packager/yaml.go @@ -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) } diff --git a/src/types/package.go b/src/types/package.go index dcfa8619a0..d996a64031 100644 --- a/src/types/package.go +++ b/src/types/package.go @@ -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. diff --git a/src/ui/lib/api-types.ts b/src/ui/lib/api-types.ts index f0724ea993..8ce498a114 100644 --- a/src/ui/lib/api-types.ts +++ b/src/ui/lib/api-types.ts @@ -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 */ @@ -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("") }, diff --git a/zarf.schema.json b/zarf.schema.json index fa3e35977f..fa97af3d7f 100644 --- a/zarf.schema.json +++ b/zarf.schema.json @@ -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,