diff --git a/.github/workflows/test-upgrade.yml b/.github/workflows/test-upgrade.yml index bafbcf3760..219eaa2147 100644 --- a/.github/workflows/test-upgrade.yml +++ b/.github/workflows/test-upgrade.yml @@ -69,7 +69,7 @@ jobs: - name: Initialize the cluster with the release version # NOTE: "PATH=$PATH" preserves the default user $PATH. This is needed to maintain the version of zarf installed - # in a previous step. This test run will the current release to create a K3s cluster. + # in a previous step. This test run will use the current release to create a K3s cluster. run: | sudo env "PATH=$PATH" CI=true zarf init --components k3s,git-server,logging --nodeport 31337 --confirm @@ -79,7 +79,7 @@ jobs: - name: Create and deploy the upgrade test packages # NOTE: "PATH=$PATH" preserves the default user $PATH. This is needed to maintain the version of zarf installed - # in a previous step. This test run will the current release to create a K3s cluster. + # in a previous step. This test run will use the current release to create a K3s cluster. run: | zarf package create src/test/upgrade --set PODINFO_VERSION=6.3.3 --confirm sudo env "PATH=$PATH" CI=true zarf package deploy zarf-package-test-upgrade-package-amd64-6.3.3.tar.zst --confirm @@ -105,12 +105,10 @@ jobs: - name: Run the upgrade tests # NOTE: "PATH=$PATH" preserves the default user $PATH. This is needed to maintain the version of zarf installed - # in a previous step. This test run will the current release to create a K3s cluster. + # in a previous step. This test run will use the current release to create a K3s cluster. run: | sudo env "PATH=$PATH" CI=true zarf tools kubectl describe nodes - zarf package create src/test/upgrade --set PODINFO_VERSION=6.3.4 --confirm - sudo env "PATH=$PATH" CI=true make test-upgrade ARCH=amd64 - name: Save logs diff --git a/Makefile b/Makefile index 2f397ae821..5c6a54497d 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ else endif endif -CLI_VERSION ?= $(if $(shell git describe --tags),$(shell git describe --tags),"UnknownVersion") +CLI_VERSION ?= $(if $(shell git describe --tags),$(shell git describe --tags),"unset-development-only") BUILD_ARGS := -s -w -X github.com/defenseunicorns/zarf/src/config.CLIVersion=$(CLI_VERSION) K8S_MODULES_VER=$(subst ., ,$(subst v,,$(shell go list -f '{{.Version}}' -m k8s.io/client-go))) K8S_MODULES_MAJOR_VER=$(shell echo $$(($(firstword $(K8S_MODULES_VER)) + 1))) @@ -203,7 +203,7 @@ test-upgrade: ## Run the Zarf CLI E2E tests for an external registry and cluster @test -s $(ZARF_BIN) || $(MAKE) build-cli [ -n "$(shell zarf version)" ] || (echo "Zarf must be installed prior to the upgrade test" && exit 1) [ -n "$(shell zarf package list 2>&1 | grep test-upgrade-package)" ] || (echo "Zarf must be initialized and have the 6.3.3 upgrade-test package installed prior to the upgrade test" && exit 1) - @test -s "zarf-package-test-upgrade-package-amd64-6.3.4.tar.zst" || zarf package create src/test/upgrade/ --set PODINFO_VERSION=6.3.4 --confirm + @test -s ./zarf-package-test-upgrade-package-$(ARCH)-6.3.4.tar.zst || zarf package create src/test/upgrade --set PODINFO_VERSION=6.3.4 --confirm cd src/test/upgrade && go test -failfast -v -timeout 30m .PHONY: test-unit diff --git a/adr/0025-required-optional.md b/adr/0025-required-optional.md new file mode 100644 index 0000000000..5b82cca96e --- /dev/null +++ b/adr/0025-required-optional.md @@ -0,0 +1,127 @@ +# 25. Components can be required by default + introduction of feature flags + +Date: 2024-04-29 + +## Status + +Accepted + +## Context + +> Feature request: + +Currently, all Zarf components default to being optional due to the `required` key being _optional_ in the YAML. This leads to package authors needing to ensure that they annotate this key for each component, and since nothing in the current validations prompts them about this they may be confused about the "all things are optional" default state. + +When Zarf was first created, we didn't really know how it would evolve and this key was introduced in those very early days. At this point it would be better to require all components by default--especially with the introduction of composability and the OCI skeleton work, there is plenty of flexibility in the API to compose bespoke packages assembled from other packages. + +A few ways to handle this: + +1. Simply force the `required` key to be a non-optional, so that package authors would be forced to specify it for each component, thereby removing any ambiguity--but also force one more key for every single component ever created 🫠 + +2. Deprecate `required` and introduce an optional `optional` key, which would default to _false_. + +3. Do something more significant like combine various condition-based things such as `only`, `optional` (instead of `required`), or `default`. + +4. Introduce `feature` flags to allow for certain schema behavior to be configurable to the user. + +## Decision + +Option 4. Introduce `.metadata.features` flags to `zarf.yaml` to allow for certain schema behavior to be configurable to the user. + +The `features` key will be added to the `metadata` section of the package schema. This key will be an array of strings, where each string is the name of a beta feature that can be enabled. To enable a feature, the user will need to add the name of the feature to this array and edit the package schema accordingly. + +> Such feature migrations can also be accomplished using `zarf dev migrate`, see the [Consequences](#consequences) section for more information. + +e.g. + +```diff +kind: ZarfInitConfig +metadata: + name: init + description: Used to establish a new Zarf cluster ++ features: ++ - default-required + +components: + - name: k3s ++ required: false + import: + path: packages/distros/k3s + + # This package moves the injector & registries binaries + - name: zarf-injector +- required: true + import: + path: packages/zarf-registry +``` + +## Consequences + +The introduction of feature flags will allow Zarf to introduce new features and schema behavior without breaking existing packages and schemas, but also introduces more complexity. This will require more documentation and user education to ensure that users understand how to use these flags. + +Beta feature flags will become the default behavior of Zarf upon the next major release, and will _not_ be configurable by the user at that time. This will allow for a more consistent experience across all Zarf packages. + +There will be a flag added to the `zarf dev migrate` command `--enable-feature ` to allow users to enable features on a per-package basis. This will allow users to test new features in a controlled environment before they are enabled by default. + +> Tab autocompletion for the `--enable-feature` flag is enabled for the `zarf dev migrate` command. + +e.g. (some output omitted for brevity) + +```bash +$ zarf dev migrate --enable-feature default-required > migrated-zarf.yaml + + NOTE Using config file ... + + NOTE Saving log file to ... + + + Migration | Type | Affected + default-required | feature | . + +``` + +```diff +$ git diff --no-index zarf.yaml migrated-zarf.yaml + +kind: ZarfInitConfig + metadata: + name: init + description: Used to establish a new Zarf cluster ++ features: ++ - default-required + components: + - name: k3s ++ required: false + import: + path: packages/distros/k3s + + - name: zarf-injector +- required: true + import: + path: packages/zarf-registry + + - name: zarf-seed-registry +- required: true + import: + path: packages/zarf-registry + + - name: zarf-registry +- required: true + import: + path: packages/zarf-registry + + - name: zarf-agent +- required: true + import: + path: packages/zarf-agent + + - name: logging ++ required: false + import: + path: packages/logging-pgl + + - name: git-server ++ required: false + import: + path: packages/gitea +``` diff --git a/packages/distros/k3s/zarf.yaml b/packages/distros/k3s/zarf.yaml index 0813b1ee38..d1980ab59b 100644 --- a/packages/distros/k3s/zarf.yaml +++ b/packages/distros/k3s/zarf.yaml @@ -1,7 +1,10 @@ kind: ZarfInitConfig metadata: name: distro-k3s - description: Used to establish a new Zarf cluster + description: | + Used to establish a new Zarf cluster + + This package is NOT meant to be used as a standalone package. It is meant to be used as a dependency of an 'init' package. components: # AMD-64 version of the K3s stack diff --git a/packages/gitea/zarf.yaml b/packages/gitea/zarf.yaml index 2f59bebdba..ef2ed54d02 100644 --- a/packages/gitea/zarf.yaml +++ b/packages/gitea/zarf.yaml @@ -1,6 +1,8 @@ kind: ZarfPackageConfig metadata: - name: init-package-git-server + name: git-server + description: | + This package is NOT meant to be used as a standalone package. It is meant to be used as a dependency of an 'init' package. variables: - name: GIT_SERVER_EXISTING_PVC diff --git a/packages/logging-pgl/zarf.yaml b/packages/logging-pgl/zarf.yaml index d5fc213dc3..6fd8edc5a8 100644 --- a/packages/logging-pgl/zarf.yaml +++ b/packages/logging-pgl/zarf.yaml @@ -1,6 +1,10 @@ kind: ZarfPackageConfig metadata: - name: init-package-logging + name: logging + description: | + Deploys the Promtail Grafana & Loki (PGL) stack. + + This package is NOT meant to be used as a standalone package. It is meant to be used as a dependency of an 'init' package. components: - name: logging diff --git a/packages/zarf-agent/zarf.yaml b/packages/zarf-agent/zarf.yaml index 32a6c34997..c3f22f6ed9 100644 --- a/packages/zarf-agent/zarf.yaml +++ b/packages/zarf-agent/zarf.yaml @@ -1,7 +1,10 @@ kind: ZarfPackageConfig metadata: - name: init-package-zarf-agent - description: Install the zarf agent mutating webhook on a new cluster + name: zarf-agent + description: | + Install the zarf agent mutating webhook on a new cluster + + This package is NOT meant to be used as a standalone package. It is meant to be used as a dependency of an 'init' package. constants: - name: AGENT_IMAGE @@ -16,7 +19,6 @@ components: images and git repository references in Kubernetes manifests. This prevents the need to manually update URLs from their original sources to the Zarf-managed docker registry and git server. - required: true images: - "###ZARF_PKG_TMPL_AGENT_IMAGE_DOMAIN######ZARF_PKG_TMPL_AGENT_IMAGE###:###ZARF_PKG_TMPL_AGENT_IMAGE_TAG###" manifests: diff --git a/packages/zarf-registry/zarf.yaml b/packages/zarf-registry/zarf.yaml index 2da03ccda6..34a4b67450 100644 --- a/packages/zarf-registry/zarf.yaml +++ b/packages/zarf-registry/zarf.yaml @@ -1,6 +1,11 @@ kind: ZarfPackageConfig metadata: - name: init-package-zarf-registry + name: zarf-registry + description: | + Initializes the Zarf Registry by bootstrapping a Kubernetes cluster with a running pod and hosting the registry image. + The Zarf Registry is then updated to use the self-hosted registry image. + + This package is NOT meant to be used as a standalone package. It is meant to be used as a dependency of an 'init' package. variables: - name: REGISTRY_EXISTING_PVC @@ -105,7 +110,6 @@ components: description: | Bootstraps a Kubernetes cluster by cloning a running pod in the cluster and hosting the registry image. Removed and destroyed after the Zarf Registry is self-hosting the registry image. - required: true only: cluster: architecture: amd64 @@ -120,7 +124,6 @@ components: description: | Bootstraps a Kubernetes cluster by cloning a running pod in the cluster and hosting the registry image. Removed and destroyed after the Zarf Registry is self-hosting the registry image. - required: true only: cluster: architecture: arm64 diff --git a/site/src/content/docs/commands/zarf_dev.md b/site/src/content/docs/commands/zarf_dev.md index a12090183d..c226b12b70 100644 --- a/site/src/content/docs/commands/zarf_dev.md +++ b/site/src/content/docs/commands/zarf_dev.md @@ -37,6 +37,7 @@ Commands useful for developing packages * [zarf dev generate](/commands/zarf_dev_generate/) - [alpha] Creates a zarf.yaml automatically from a given remote (git) Helm chart * [zarf dev generate-config](/commands/zarf_dev_generate-config/) - Generates a config file for Zarf * [zarf dev lint](/commands/zarf_dev_lint/) - Lints the given package for valid schema and recommended practices +* [zarf dev migrate](/commands/zarf_dev_migrate/) - [alpha] Migrates the zarf.yaml in a given directory to the latest version of the zarf.yaml format * [zarf dev patch-git](/commands/zarf_dev_patch-git/) - Converts all .git URLs to the specified Zarf HOST and with the Zarf URL pattern in a given FILE. NOTE: This should only be used for manifests that are not mutated by the Zarf Agent Mutating Webhook. * [zarf dev sha256sum](/commands/zarf_dev_sha256sum/) - Generates a SHA256SUM for the given file diff --git a/site/src/content/docs/commands/zarf_dev_migrate.md b/site/src/content/docs/commands/zarf_dev_migrate.md new file mode 100644 index 0000000000..189f52e6f9 --- /dev/null +++ b/site/src/content/docs/commands/zarf_dev_migrate.md @@ -0,0 +1,41 @@ +--- +title: zarf dev migrate +description: Zarf CLI command reference for zarf dev migrate. +tableOfContents: false +--- + + + +## zarf dev migrate + +[alpha] Migrates the zarf.yaml in a given directory to the latest version of the zarf.yaml format + +``` +zarf dev migrate [flags] +``` + +### Options + +``` + --enable-feature stringArray feature migrations to run and enable (available: default-required) + -h, --help help for migrate + --run stringArray migrations of deprecated features to run (default: all, available: scripts-to-actions, pluralize-set-variable) +``` + +### Options inherited from parent commands + +``` + -a, --architecture string Architecture for OCI images and Zarf packages + --insecure Allow access to insecure registries and disable other recommended security enforcements such as package checksum and signature validation. This flag should only be used if you have a specific reason and accept the reduced security posture. + -l, --log-level string Log level when running Zarf. Valid options are: warn, info, debug, trace (default "info") + --no-color Disable colors in output + --no-log-file Disable log file creation + --no-progress Disable fancy UI progress bars, spinners, logos, etc + --tmpdir string Specify the temporary directory to use for intermediate files + --zarf-cache string Specify the location of the Zarf cache directory (default "~/.zarf-cache") +``` + +### SEE ALSO + +* [zarf dev](/commands/zarf_dev/) - Commands useful for developing packages + diff --git a/src/cmd/dev.go b/src/cmd/dev.go index 17962176a4..42fa41977a 100644 --- a/src/cmd/dev.go +++ b/src/cmd/dev.go @@ -9,16 +9,24 @@ import ( "io" "os" "path/filepath" + "reflect" + "slices" "strings" + "github.com/fatih/color" + goyaml "github.com/goccy/go-yaml" + "github.com/pterm/pterm" + "github.com/AlecAivazis/survey/v2" "github.com/defenseunicorns/pkg/helpers" "github.com/defenseunicorns/zarf/src/cmd/common" "github.com/defenseunicorns/zarf/src/config" "github.com/defenseunicorns/zarf/src/config/lang" + "github.com/defenseunicorns/zarf/src/pkg/layout" "github.com/defenseunicorns/zarf/src/pkg/message" "github.com/defenseunicorns/zarf/src/pkg/packager" "github.com/defenseunicorns/zarf/src/pkg/packager/lint" + "github.com/defenseunicorns/zarf/src/pkg/packager/migrations" "github.com/defenseunicorns/zarf/src/pkg/transform" "github.com/defenseunicorns/zarf/src/pkg/utils" "github.com/defenseunicorns/zarf/src/types" @@ -28,6 +36,8 @@ import ( ) var extractPath string +var deprecatedMigrationsToRun []string +var featureMigrationsToRun []string var devCmd = &cobra.Command{ Use: "dev", @@ -59,6 +69,89 @@ var devDeployCmd = &cobra.Command{ }, } +var devMigrateCmd = &cobra.Command{ + Use: "migrate", + Short: lang.CmdDevMigrateShort, + Args: cobra.MaximumNArgs(1), + Run: func(_ *cobra.Command, args []string) { + dir := common.SetBaseDirectory(args) + var pkg types.ZarfPackage + cm := goyaml.CommentMap{} + + before, err := os.ReadFile(filepath.Join(dir, layout.ZarfYAML)) + if err != nil { + message.Fatalf(err, lang.CmdDevMigrateErr, err.Error()) + } + + if err := goyaml.UnmarshalWithOptions(before, &pkg, goyaml.CommentToMap(cm)); err != nil { + message.Fatalf(err, lang.CmdDevMigrateErr, err.Error()) + } + + data := [][]string{} + + deprecatedMigrationsToRun = slices.Compact(deprecatedMigrationsToRun) + for _, m := range migrations.DeprecatedComponentMigrations() { + if !slices.Contains(deprecatedMigrationsToRun, m.String()) && len(deprecatedMigrationsToRun) != 0 { + continue + } + + entry := []string{ + m.String(), + message.ColorWrap("deprecated", color.FgYellow), + "", + } + + for idx, component := range pkg.Components { + mc, _ := m.Run(component) + mc = m.Clear(mc) + if !reflect.DeepEqual(mc, component) { + entry[2] = fmt.Sprintf(".components.[%d]", idx) + data = append(data, entry) + } + pkg.Components[idx] = mc + } + } + + featureMigrationsToRun = slices.Compact(featureMigrationsToRun) + for _, m := range migrations.FeatureMigrations() { + if !slices.Contains(featureMigrationsToRun, m.String()) { + continue + } + pkgWithFeature, warning := m.Run(pkg) + if warning != "" { + message.Warn(warning) + } + if !reflect.DeepEqual(pkgWithFeature, pkg) { + data = append(data, []string{ + m.String(), + message.ColorWrap("feature", color.FgMagenta), + ".", + }) + } + pkg = pkgWithFeature + } + + after, err := goyaml.MarshalWithOptions(pkg, goyaml.WithComment(cm), goyaml.IndentSequence(true), goyaml.UseSingleQuote(false)) + if err != nil { + message.Fatalf(err, lang.CmdDevMigrateErr, err.Error()) + } + + header := []string{ + "Migration", + "Type", + "Affected Path(s)", + } + + if len(data) == 0 { + message.Warn("No changes made") + } else { + pterm.Println() + fmt.Println(string(after)) + message.Table(header, data) + } + }, +} + var devGenerateCmd = &cobra.Command{ Use: "generate NAME", Aliases: []string{"g"}, @@ -270,6 +363,7 @@ func init() { rootCmd.AddCommand(devCmd) devCmd.AddCommand(devDeployCmd) + devCmd.AddCommand(devMigrateCmd) devCmd.AddCommand(devGenerateCmd) devCmd.AddCommand(devTransformGitLinksCmd) devCmd.AddCommand(devSha256SumCmd) @@ -280,6 +374,38 @@ func init() { bindDevDeployFlags(v) bindDevGenerateFlags(v) + cm := []string{} + for _, m := range migrations.DeprecatedComponentMigrations() { + cm = append(cm, m.String()) + } + devMigrateCmd.Flags().StringArrayVar(&deprecatedMigrationsToRun, "run", []string{}, fmt.Sprintf("migrations of deprecated features to run (default: all, available: %s)", strings.Join(cm, ", "))) + devMigrateCmd.RegisterFlagCompletionFunc("run", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + ids := []string{} + for _, m := range migrations.DeprecatedComponentMigrations() { + if slices.Contains(deprecatedMigrationsToRun, m.String()) { + continue + } + ids = append(ids, m.String()) + } + return ids, cobra.ShellCompDirectiveNoFileComp + }) + + fm := []string{} + for _, m := range migrations.FeatureMigrations() { + fm = append(fm, m.String()) + } + devMigrateCmd.Flags().StringArrayVar(&featureMigrationsToRun, "enable-feature", []string{}, fmt.Sprintf("feature migrations to run and enable (available: %s)", strings.Join(fm, ", "))) + devMigrateCmd.RegisterFlagCompletionFunc("enable-feature", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + ids := []string{} + for _, m := range migrations.FeatureMigrations() { + if slices.Contains(featureMigrationsToRun, m.String()) { + continue + } + ids = append(ids, m.String()) + } + return ids, cobra.ShellCompDirectiveNoFileComp + }) + devSha256SumCmd.Flags().StringVarP(&extractPath, "extract-path", "e", "", lang.CmdDevFlagExtractPath) devFindImagesCmd.Flags().StringVarP(&pkgConfig.FindImagesOpts.RepoHelmChartPath, "repo-chart-path", "p", "", lang.CmdDevFlagRepoChartPath) diff --git a/src/config/lang/english.go b/src/config/lang/english.go index adae1f1a41..1c35de2f7a 100644 --- a/src/config/lang/english.go +++ b/src/config/lang/english.go @@ -360,6 +360,19 @@ $ zarf package pull oci://ghcr.io/defenseunicorns/packages/dos-games:1.0.0 -a sk CmdDevDeployFlagNoYolo = "Disable the YOLO mode default override and create / deploy the package as-defined" CmdDevDeployErr = "Failed to dev deploy: %s" + CmdDevMigrateShort = "[alpha] Migrates the zarf.yaml in a given directory to the latest version of the zarf.yaml format" + CmdDevMigrateExample = ` +# Migrate the zarf.yaml in the current directory +$ zarf dev migrate . + +# Run specific migrations +$ zarf dev migrate --run scripts-to-actions --run pluralize-set-variable . + +# Enable feature flag(s) and run migrations +$ zarf dev migrate --enable-feature default-required . +` + CmdDevMigrateErr = "Failed to migrate: %s" + CmdDevGenerateShort = "[alpha] Creates a zarf.yaml automatically from a given remote (git) Helm chart" CmdDevGenerateExample = "zarf dev generate podinfo --url https://github.com/stefanprodan/podinfo.git --version 6.4.0 --gitPath charts/podinfo" @@ -687,6 +700,7 @@ const ( // Package validate const ( + PkgValidateErrBetaFeatureNotFound = "feature %q not found, available: %s" PkgValidateTemplateDeprecation = "Package template %q is using the deprecated syntax ###ZARF_PKG_VAR_%s###. This will be removed in Zarf v1.0.0. Please update to ###ZARF_PKG_TMPL_%s###." PkgValidateMustBeUppercase = "variable name %q must be all uppercase and contain no special characters except _" PkgValidateErrAction = "invalid action: %w" @@ -704,6 +718,7 @@ const ( PkgValidateErrComponentLocalOS = "component %q contains a localOS value that is not supported: %s (supported: %s)" PkgValidateErrComponentNameNotUnique = "component name %q is not unique" PkgValidateErrComponent = "invalid component %q: %w" + PkgValidateErrComponentMissingGroup = "component %q cannot use default without a group" PkgValidateErrComponentReqDefault = "component %q cannot be both required and default" PkgValidateErrComponentReqGrouped = "component %q cannot be both required and grouped" PkgValidateErrComponentYOLO = "component %q incompatible with the online-only package flag (metadata.yolo): %w" diff --git a/src/pkg/interactive/components.go b/src/pkg/interactive/select.go similarity index 100% rename from src/pkg/interactive/components.go rename to src/pkg/interactive/select.go diff --git a/src/pkg/layout/package.go b/src/pkg/layout/package.go index 3949f4fcc2..6a4266d53e 100644 --- a/src/pkg/layout/package.go +++ b/src/pkg/layout/package.go @@ -15,7 +15,7 @@ import ( "github.com/defenseunicorns/pkg/helpers" "github.com/defenseunicorns/zarf/src/pkg/interactive" "github.com/defenseunicorns/zarf/src/pkg/message" - "github.com/defenseunicorns/zarf/src/pkg/packager/deprecated" + "github.com/defenseunicorns/zarf/src/pkg/packager/migrations" "github.com/defenseunicorns/zarf/src/pkg/utils" "github.com/defenseunicorns/zarf/src/types" "github.com/google/go-containerregistry/pkg/crane" @@ -69,11 +69,21 @@ func (pp *PackagePaths) ReadZarfYAML() (pkg types.ZarfPackage, warnings []string } if len(pkg.Build.Migrations) > 0 { - var componentWarnings []string for idx, component := range pkg.Components { - // Handle component configuration deprecations - pkg.Components[idx], componentWarnings = deprecated.MigrateComponent(pkg.Build, component) - warnings = append(warnings, componentWarnings...) + // Clear out component configuration migrations + for _, m := range migrations.DeprecatedComponentMigrations() { + if slices.Contains(pkg.Build.Migrations, m.String()) { + mc := m.Clear(component) + pkg.Components[idx] = mc + } + } + } + } + + for _, component := range pkg.Components { + // Show a warning if the component contains a group as that has been deprecated and will be removed. + if component.DeprecatedGroup != "" { + warnings = append(warnings, fmt.Sprintf("Component %s is using group which has been deprecated and will be removed in v1.0.0. Please migrate to another solution.", component.Name)) } } diff --git a/src/pkg/packager/common.go b/src/pkg/packager/common.go index f432b8d423..3b8b813543 100644 --- a/src/pkg/packager/common.go +++ b/src/pkg/packager/common.go @@ -23,7 +23,7 @@ import ( "github.com/defenseunicorns/zarf/src/config" "github.com/defenseunicorns/zarf/src/pkg/layout" "github.com/defenseunicorns/zarf/src/pkg/message" - "github.com/defenseunicorns/zarf/src/pkg/packager/deprecated" + "github.com/defenseunicorns/zarf/src/pkg/packager/migrations" "github.com/defenseunicorns/zarf/src/pkg/packager/sources" "github.com/defenseunicorns/zarf/src/pkg/utils" ) @@ -212,7 +212,7 @@ func (p *Packager) attemptClusterChecks(ctx context.Context) (err error) { // Check for any breaking changes between the initialized Zarf version and this CLI if existingInitPackage, _ := p.cluster.GetDeployedPackage(ctx, "init"); existingInitPackage != nil { // Use the build version instead of the metadata since this will support older Zarf versions - deprecated.PrintBreakingChanges(existingInitPackage.Data.Build.Version) + migrations.PrintBreakingChanges(existingInitPackage.Data.Build.Version) } spinner.Success() diff --git a/src/pkg/packager/composer/list.go b/src/pkg/packager/composer/list.go index 4f1a484ca3..1132568d08 100644 --- a/src/pkg/packager/composer/list.go +++ b/src/pkg/packager/composer/list.go @@ -13,7 +13,7 @@ import ( "github.com/defenseunicorns/pkg/helpers" "github.com/defenseunicorns/zarf/src/extensions/bigbang" "github.com/defenseunicorns/zarf/src/pkg/layout" - "github.com/defenseunicorns/zarf/src/pkg/packager/deprecated" + "github.com/defenseunicorns/zarf/src/pkg/packager/migrations" "github.com/defenseunicorns/zarf/src/pkg/utils" "github.com/defenseunicorns/zarf/src/pkg/variables" "github.com/defenseunicorns/zarf/src/pkg/zoci" @@ -257,12 +257,16 @@ func (ic *ImportChain) String() string { } // Migrate performs migrations on the import chain -func (ic *ImportChain) Migrate(build types.ZarfBuildData) (warnings []string) { +func (ic *ImportChain) Migrate() (warnings []string) { node := ic.head for node != nil { - migrated, w := deprecated.MigrateComponent(build, node.ZarfComponent) - node.ZarfComponent = migrated - warnings = append(warnings, w...) + for _, m := range migrations.DeprecatedComponentMigrations() { + migrated, warning := m.Run(node.ZarfComponent) + node.ZarfComponent = migrated + if warning != "" { + warnings = append(warnings, warning) + } + } node = node.next } if len(warnings) > 0 { diff --git a/src/pkg/packager/creator/compose.go b/src/pkg/packager/creator/compose.go index dd2e429731..cc1c93e390 100644 --- a/src/pkg/packager/creator/compose.go +++ b/src/pkg/packager/creator/compose.go @@ -38,7 +38,7 @@ func ComposeComponents(pkg types.ZarfPackage, flavor string) (types.ZarfPackage, message.Debugf("%s", chain) // migrate any deprecated component configurations now - warning := chain.Migrate(pkg.Build) + warning := chain.Migrate() warnings = append(warnings, warning...) // get the composed component diff --git a/src/pkg/packager/creator/utils.go b/src/pkg/packager/creator/utils.go index 43ab469565..7ad4b6c8b0 100644 --- a/src/pkg/packager/creator/utils.go +++ b/src/pkg/packager/creator/utils.go @@ -10,7 +10,7 @@ import ( "time" "github.com/defenseunicorns/zarf/src/config" - "github.com/defenseunicorns/zarf/src/pkg/packager/deprecated" + "github.com/defenseunicorns/zarf/src/pkg/packager/migrations" "github.com/defenseunicorns/zarf/src/types" ) @@ -43,10 +43,10 @@ func recordPackageMetadata(pkg *types.ZarfPackage, createOpts types.ZarfCreateOp // Record the time of package creation. pkg.Build.Timestamp = now.Format(time.RFC1123Z) - // Record the migrations that will be ran on the package. - pkg.Build.Migrations = []string{ - deprecated.ScriptsToActionsMigrated, - deprecated.PluralizeSetVariable, + // Record the migrations that will run on the package. + pkg.Build.Migrations = []string{} + for _, m := range migrations.DeprecatedComponentMigrations() { + pkg.Build.Migrations = append(pkg.Build.Migrations, m.String()) } // Record the flavor of Zarf used to build this package (if any). @@ -55,7 +55,7 @@ func recordPackageMetadata(pkg *types.ZarfPackage, createOpts types.ZarfCreateOp pkg.Build.RegistryOverrides = createOpts.RegistryOverrides // Record the latest version of Zarf without breaking changes to the package structure. - pkg.Build.LastNonBreakingVersion = deprecated.LastNonBreakingVersion + pkg.Build.LastNonBreakingVersion = migrations.LastNonBreakingVersion return nil } diff --git a/src/pkg/packager/filters/deploy.go b/src/pkg/packager/filters/deploy.go index 622f481229..ecac84efe5 100644 --- a/src/pkg/packager/filters/deploy.go +++ b/src/pkg/packager/filters/deploy.go @@ -33,7 +33,7 @@ type deploymentFilter struct { // Errors for the deployment filter. var ( - ErrMultipleSameGroup = fmt.Errorf("cannot specify multiple components from the same group") + ErrMultipleSameGroup = fmt.Errorf("cannot specify multiple components") ErrNoDefaultOrSelection = fmt.Errorf("no default or selected component found") ErrNotFound = fmt.Errorf("no compatible components found") ErrSelectionCanceled = fmt.Errorf("selection canceled") @@ -64,7 +64,6 @@ func (f *deploymentFilter) Apply(pkg types.ZarfPackage) ([]types.ZarfComponent, if isPartial { matchedRequests := map[string]bool{} - // NOTE: This does not use forIncludedComponents as it takes group, default and required status into account. for _, groupKey := range orderedComponentGroups { var groupDefault *types.ZarfComponent var groupSelected *types.ZarfComponent @@ -75,7 +74,7 @@ func (f *deploymentFilter) Apply(pkg types.ZarfPackage) ([]types.ZarfComponent, selectState, matchedRequest := includedOrExcluded(component.Name, f.requestedComponents) - if !component.IsRequired() { + if !component.IsRequired(pkg.Metadata.Features) { if selectState == excluded { // If the component was explicitly excluded, record the match and continue matchedRequests[matchedRequest] = true @@ -95,7 +94,7 @@ func (f *deploymentFilter) Apply(pkg types.ZarfPackage) ([]types.ZarfComponent, // Then check for already selected groups if groupSelected != nil { - return nil, fmt.Errorf("%w: group: %s selected: %s, %s", ErrMultipleSameGroup, component.DeprecatedGroup, groupSelected.Name, component.Name) + return nil, fmt.Errorf("%w: group: %q selected: %q, %s", ErrMultipleSameGroup, component.DeprecatedGroup, groupSelected.Name, component.Name) } // Then append to the final list @@ -161,7 +160,7 @@ func (f *deploymentFilter) Apply(pkg types.ZarfPackage) ([]types.ZarfComponent, } else { component := groupedComponents[groupKey][0] - if component.IsRequired() { + if component.IsRequired(pkg.Metadata.Features) { selectedComponents = append(selectedComponents, component) continue } diff --git a/src/pkg/packager/filters/deploy_test.go b/src/pkg/packager/filters/deploy_test.go index 310ed6286b..cc3a96e01e 100644 --- a/src/pkg/packager/filters/deploy_test.go +++ b/src/pkg/packager/filters/deploy_test.go @@ -49,62 +49,46 @@ func componentFromQuery(t *testing.T, q string) types.ZarfComponent { return c } -func componentMatrix(_ *testing.T) []types.ZarfComponent { +func componentMatrix(t *testing.T) []types.ZarfComponent { var components []types.ZarfComponent defaultValues := []bool{true, false} requiredValues := []interface{}{nil, true, false} - // the duplicate groups are intentional - // this is to test group membership + default filtering - groupValues := []string{"", "foo", "foo", "foo", "bar", "bar", "bar"} - - for idx, groupValue := range groupValues { - for _, defaultValue := range defaultValues { - for _, requiredValue := range requiredValues { - name := strings.Builder{} - - // per validate rules, components in groups cannot be required - if requiredValue != nil && requiredValue.(bool) == true && groupValue != "" { - continue - } - name.WriteString(fmt.Sprintf("required=%v", requiredValue)) + // all possible combinations of default, required, and optional + for _, defaultValue := range defaultValues { + for _, requiredValue := range requiredValues { - if groupValue != "" { - name.WriteString(fmt.Sprintf(" && group=%s && idx=%d && default=%t", groupValue, idx, defaultValue)) - } else if defaultValue { - name.WriteString(" && default=true") - } + // per validate, components cannot be both default and required + if defaultValue == true && requiredValue == true { + continue + } - if groupValue != "" { - // if there already exists a component in this group that is default, then set the default to false - // otherwise the filter will error - defaultAlreadyExists := false - if defaultValue { - for _, c := range components { - if c.DeprecatedGroup == groupValue && c.Default { - defaultAlreadyExists = true - break - } - } - } - if defaultAlreadyExists { - defaultValue = false - } - } + query := fmt.Sprintf("required=%v", requiredValue) - c := types.ZarfComponent{ - Name: name.String(), - Default: defaultValue, - DeprecatedGroup: groupValue, - } + if defaultValue { + query = fmt.Sprintf("%s && default=true", query) + } - if requiredValue != nil { - c.Required = helpers.BoolPtr(requiredValue.(bool)) - } + c := componentFromQuery(t, query) + components = append(components, c) + } + } - components = append(components, c) + members := 3 + for _, group := range []string{"foo", "bar"} { + for i := 0; i < members; i++ { + var defaultValue bool + // ensure there is only one default per group + // this enforced on `zarf package create`'s validate + if i == 0 { + defaultValue = true } + c := componentFromQuery(t, fmt.Sprintf("group=%s && idx=%d && default=%t", group, i, defaultValue)) + // due to validation on create, there will not be a case where + // c.Default == true && c.Required == true) + c.Required = nil + components = append(components, c) } } @@ -121,61 +105,50 @@ func TestDeployFilter_Apply(t *testing.T) { want []types.ZarfComponent expectedErr error }{ - "Test when version is less than v0.33.0 w/ no optional components selected": { + "Test when no optional components selected": { pkg: types.ZarfPackage{ - Build: types.ZarfBuildData{ - Version: "v0.32.0", - }, Components: possibilities, }, optionalComponents: "", want: []types.ZarfComponent{ componentFromQuery(t, "required= && default=true"), - componentFromQuery(t, "required=true && default=true"), componentFromQuery(t, "required=false && default=true"), componentFromQuery(t, "required=true"), - componentFromQuery(t, "required= && group=foo && idx=1 && default=true"), - componentFromQuery(t, "required= && group=bar && idx=4 && default=true"), + componentFromQuery(t, "group=foo && idx=0 && default=true"), + componentFromQuery(t, "group=bar && idx=0 && default=true"), }, }, - "Test when version is less than v0.33.0 w/ some optional components selected": { + "Test when some optional components selected": { pkg: types.ZarfPackage{ - Build: types.ZarfBuildData{ - Version: "v0.32.0", - }, Components: possibilities, }, - optionalComponents: strings.Join([]string{"required=false", "required= && group=bar && idx=5 && default=false", "-required=true"}, ","), + optionalComponents: strings.Join([]string{ + "required=false", + "group=bar && idx=2 && default=false", + "-required=true", + }, ","), want: []types.ZarfComponent{ componentFromQuery(t, "required= && default=true"), - componentFromQuery(t, "required=true && default=true"), componentFromQuery(t, "required=false && default=true"), - // while "required=true" was deselected, it is still required - // therefore it should be included - componentFromQuery(t, "required=true"), - componentFromQuery(t, "required=false"), - componentFromQuery(t, "required= && group=foo && idx=1 && default=true"), - componentFromQuery(t, "required= && group=bar && idx=5 && default=false"), + componentFromQuery(t, "required=true"), // required components cannot be deselected + componentFromQuery(t, "required=false"), // optional components can be selected + componentFromQuery(t, "group=foo && idx=0 && default=true"), + componentFromQuery(t, "group=bar && idx=2 && default=false"), // components within a group can be selected, the default is not selected }, }, "Test failing when group has no default and no selection was made": { pkg: types.ZarfPackage{ - Build: types.ZarfBuildData{ - Version: "v0.32.0", - }, Components: []types.ZarfComponent{ + componentFromQuery(t, "group=foo && default=true"), componentFromQuery(t, "group=foo && default=false"), componentFromQuery(t, "group=foo && default=false"), }, }, - optionalComponents: "", + optionalComponents: "-group=foo && default=true", expectedErr: ErrNoDefaultOrSelection, }, "Test failing when multiple are selected from the same group": { pkg: types.ZarfPackage{ - Build: types.ZarfBuildData{ - Version: "v0.32.0", - }, Components: []types.ZarfComponent{ componentFromQuery(t, "group=foo && default=true"), componentFromQuery(t, "group=foo && default=false"), @@ -186,14 +159,30 @@ func TestDeployFilter_Apply(t *testing.T) { }, "Test failing when no components are found that match the query": { pkg: types.ZarfPackage{ - Build: types.ZarfBuildData{ - Version: "v0.32.0", - }, Components: possibilities, }, optionalComponents: "nonexistent", expectedErr: ErrNotFound, }, + "[default-required] Test when no optional components selected": { + pkg: types.ZarfPackage{ + Metadata: types.ZarfMetadata{ + Features: []types.FeatureFlag{ + types.DefaultRequired, + }, + }, + Components: possibilities, + }, + optionalComponents: "", + want: []types.ZarfComponent{ + componentFromQuery(t, "required= && default=true"), + componentFromQuery(t, "required=false && default=true"), + componentFromQuery(t, "required="), + componentFromQuery(t, "required=true"), + componentFromQuery(t, "group=foo && idx=0 && default=true"), + componentFromQuery(t, "group=bar && idx=0 && default=true"), + }, + }, } for name, tc := range testCases { diff --git a/src/pkg/packager/deprecated/common.go b/src/pkg/packager/migrations/common.go similarity index 54% rename from src/pkg/packager/deprecated/common.go rename to src/pkg/packager/migrations/common.go index 3583eac9a4..328a042593 100644 --- a/src/pkg/packager/deprecated/common.go +++ b/src/pkg/packager/migrations/common.go @@ -1,15 +1,13 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2021-Present The Zarf Authors -// Package deprecated handles package deprecations and migrations -package deprecated +// Package migrations handles component deprecations and package migrations +package migrations import ( "fmt" "strings" - "slices" - "github.com/Masterminds/semver/v3" "github.com/defenseunicorns/zarf/src/config" "github.com/defenseunicorns/zarf/src/pkg/message" @@ -17,23 +15,20 @@ import ( "github.com/pterm/pterm" ) -// BreakingChange represents a breaking change that happened on a specified Zarf version -type BreakingChange struct { +// breakingChange represents a breaking change that happened on a specified Zarf version +type breakingChange struct { version *semver.Version title string mitigation string } -// 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" -) +// LastNonBreakingVersion is the last version that did not have any breaking changes +// +// This should be updated when a breaking change is introduced to the Zarf package structure. See: https://github.com/defenseunicorns/zarf/releases/tag/v0.32.2 +const LastNonBreakingVersion = "v0.27.0" // List of breaking changes to warn the user of. -var breakingChanges = []BreakingChange{ +var breakingChanges = []breakingChange{ { version: semver.New(0, 26, 0, "", ""), title: "Zarf container images are now mutated based on tag instead of repository name.", @@ -41,40 +36,36 @@ var breakingChanges = []BreakingChange{ }, } -// MigrateComponent runs all migrations on a component. -// Build should be empty on package create, but include just in case someone copied a zarf.yaml from a zarf package. -func MigrateComponent(build types.ZarfBuildData, component types.ZarfComponent) (migratedComponent types.ZarfComponent, warnings []string) { - migratedComponent = component - - // If the component has already been migrated, clear the deprecated scripts. - if slices.Contains(build.Migrations, ScriptsToActionsMigrated) { - migratedComponent.DeprecatedScripts = types.DeprecatedZarfComponentScripts{} - } else { - // Otherwise, run the migration. - var warning string - if migratedComponent, warning = migrateScriptsToActions(migratedComponent); warning != "" { - warnings = append(warnings, warning) - } - } +// PackageOrComponent is a type that can be either a ZarfComponent or a ZarfPackage +type PackageOrComponent interface { + types.ZarfComponent | types.ZarfPackage +} - // If the component has already been migrated, clear the setVariable definitions. - if slices.Contains(build.Migrations, PluralizeSetVariable) { - migratedComponent = clearSetVariables(migratedComponent) - } else { - // Otherwise, run the migration. - var warning string - if migratedComponent, warning = migrateSetVariableToSetVariables(migratedComponent); warning != "" { - warnings = append(warnings, warning) - } - } +// Migration represents a migration on types satisfying the PackageOrComponent interface. +type Migration[T PackageOrComponent] interface { + fmt.Stringer + Run(T) (T, string) +} + +// DeprecatedMigration represents a migration on types satisfying the PackageOrComponent interface. +type DeprecatedMigration[T PackageOrComponent] interface { + Migration[T] + Clear(T) T +} - // Show a warning if the component contains a group as that has been deprecated and will be removed. - if component.DeprecatedGroup != "" { - warnings = append(warnings, fmt.Sprintf("Component %s is using group which has been deprecated and will be removed in v1.0.0. Please migrate to another solution.", component.Name)) +// DeprecatedComponentMigrations returns a list of all current deprecated component-level migrations. +func DeprecatedComponentMigrations() []DeprecatedMigration[types.ZarfComponent] { + return []DeprecatedMigration[types.ZarfComponent]{ + ScriptsToActions{}, + SetVariableToSetVariables{}, } +} - // Future migrations here. - return migratedComponent, warnings +// FeatureMigrations returns a list of all current feature migrations. +func FeatureMigrations() []Migration[types.ZarfPackage] { + return []Migration[types.ZarfPackage]{ + DefaultRequired{}, + } } // PrintBreakingChanges prints the breaking changes between the provided version and the current CLIVersion @@ -85,7 +76,7 @@ func PrintBreakingChanges(deployedZarfVersion string) { return } - applicableBreakingChanges := []BreakingChange{} + applicableBreakingChanges := []breakingChange{} // Calculate the applicable breaking changes for _, breakingChange := range breakingChanges { diff --git a/src/pkg/packager/migrations/default_required.go b/src/pkg/packager/migrations/default_required.go new file mode 100644 index 0000000000..97048dc959 --- /dev/null +++ b/src/pkg/packager/migrations/default_required.go @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +package migrations + +import ( + "fmt" + "slices" + + "github.com/defenseunicorns/pkg/helpers" + "github.com/defenseunicorns/zarf/src/types" +) + +// DefaultRequired migrates the package to change components to be required by default +type DefaultRequired struct{} + +// String returns the name of the migration +func (DefaultRequired) String() string { + return string(types.DefaultRequired) +} + +// Run sets all components to be required by default +// +// and cleanly migrates components explicitly marked as required to be nil +func (DefaultRequired) Run(pkg types.ZarfPackage) (types.ZarfPackage, string) { + if slices.Contains(pkg.Metadata.Features, types.DefaultRequired) { + return pkg, fmt.Sprintf("%s feature flag already enabled", types.DefaultRequired) + } + + pkg.Metadata.Features = append(pkg.Metadata.Features, types.DefaultRequired) + + for idx, component := range pkg.Components { + if component.Required != nil && *component.Required { + pkg.Components[idx].Required = nil + } + if component.Required == nil { + pkg.Components[idx].Required = helpers.BoolPtr(false) + } + } + + return pkg, "" +} diff --git a/src/pkg/packager/deprecated/pluralize-set-variable.go b/src/pkg/packager/migrations/pluralize_set_variable.go similarity index 58% rename from src/pkg/packager/deprecated/pluralize-set-variable.go rename to src/pkg/packager/migrations/pluralize_set_variable.go index c3dc13e06c..3a915c006c 100644 --- a/src/pkg/packager/deprecated/pluralize-set-variable.go +++ b/src/pkg/packager/migrations/pluralize_set_variable.go @@ -1,8 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2021-Present The Zarf Authors -// Package deprecated handles package deprecations and migrations -package deprecated +package migrations import ( "fmt" @@ -11,7 +10,50 @@ import ( "github.com/defenseunicorns/zarf/src/types" ) -func migrateSetVariableToSetVariables(c types.ZarfComponent) (types.ZarfComponent, string) { +// PluralizeSetVariableID is the ID of the SetVariableToSetVariables migration +const PluralizeSetVariableID = "pluralize-set-variable" + +// SetVariableToSetVariables migrates setVariable to setVariables +type SetVariableToSetVariables struct{} + +// String returns the name of the migration +func (SetVariableToSetVariables) String() string { + return PluralizeSetVariableID +} + +// Clear the deprecated setVariable. +func (SetVariableToSetVariables) Clear(mc types.ZarfComponent) types.ZarfComponent { + clear := func(actions []types.ZarfComponentAction) []types.ZarfComponentAction { + for i := range actions { + actions[i].DeprecatedSetVariable = "" + } + + return actions + } + + // Clear OnCreate SetVariables + mc.Actions.OnCreate.After = clear(mc.Actions.OnCreate.After) + mc.Actions.OnCreate.Before = clear(mc.Actions.OnCreate.Before) + mc.Actions.OnCreate.OnSuccess = clear(mc.Actions.OnCreate.OnSuccess) + mc.Actions.OnCreate.OnFailure = clear(mc.Actions.OnCreate.OnFailure) + + // Clear OnDeploy SetVariables + mc.Actions.OnDeploy.After = clear(mc.Actions.OnDeploy.After) + mc.Actions.OnDeploy.Before = clear(mc.Actions.OnDeploy.Before) + mc.Actions.OnDeploy.OnSuccess = clear(mc.Actions.OnDeploy.OnSuccess) + mc.Actions.OnDeploy.OnFailure = clear(mc.Actions.OnDeploy.OnFailure) + + // Clear OnRemove SetVariables + mc.Actions.OnRemove.After = clear(mc.Actions.OnRemove.After) + mc.Actions.OnRemove.Before = clear(mc.Actions.OnRemove.Before) + mc.Actions.OnRemove.OnSuccess = clear(mc.Actions.OnRemove.OnSuccess) + mc.Actions.OnRemove.OnFailure = clear(mc.Actions.OnRemove.OnFailure) + + return mc +} + +// Run coverts the deprecated setVariable to the new setVariables +func (SetVariableToSetVariables) Run(c types.ZarfComponent) (types.ZarfComponent, string) { hasSetVariable := false migrate := func(actions []types.ZarfComponentAction) []types.ZarfComponentAction { @@ -55,33 +97,3 @@ func migrateSetVariableToSetVariables(c types.ZarfComponent) (types.ZarfComponen return c, "" } - -func clearSetVariables(c types.ZarfComponent) types.ZarfComponent { - clear := func(actions []types.ZarfComponentAction) []types.ZarfComponentAction { - for i := range actions { - actions[i].DeprecatedSetVariable = "" - } - - return actions - } - - // Clear OnCreate SetVariables - c.Actions.OnCreate.After = clear(c.Actions.OnCreate.After) - c.Actions.OnCreate.Before = clear(c.Actions.OnCreate.Before) - c.Actions.OnCreate.OnSuccess = clear(c.Actions.OnCreate.OnSuccess) - c.Actions.OnCreate.OnFailure = clear(c.Actions.OnCreate.OnFailure) - - // Clear OnDeploy SetVariables - c.Actions.OnDeploy.After = clear(c.Actions.OnDeploy.After) - c.Actions.OnDeploy.Before = clear(c.Actions.OnDeploy.Before) - c.Actions.OnDeploy.OnSuccess = clear(c.Actions.OnDeploy.OnSuccess) - c.Actions.OnDeploy.OnFailure = clear(c.Actions.OnDeploy.OnFailure) - - // Clear OnRemove SetVariables - c.Actions.OnRemove.After = clear(c.Actions.OnRemove.After) - c.Actions.OnRemove.Before = clear(c.Actions.OnRemove.Before) - c.Actions.OnRemove.OnSuccess = clear(c.Actions.OnRemove.OnSuccess) - c.Actions.OnRemove.OnFailure = clear(c.Actions.OnRemove.OnFailure) - - return c -} diff --git a/src/pkg/packager/deprecated/scripts-to-actions.go b/src/pkg/packager/migrations/scripts_to_actions.go similarity index 74% rename from src/pkg/packager/deprecated/scripts-to-actions.go rename to src/pkg/packager/migrations/scripts_to_actions.go index 2040e7eb90..bf620e4b51 100644 --- a/src/pkg/packager/deprecated/scripts-to-actions.go +++ b/src/pkg/packager/migrations/scripts_to_actions.go @@ -1,8 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2021-Present The Zarf Authors -// Package deprecated handles package deprecations and migrations -package deprecated +package migrations import ( "fmt" @@ -11,14 +10,31 @@ import ( "github.com/defenseunicorns/zarf/src/types" ) -// migrateScriptsToActions coverts the deprecated scripts to the new actions +// ScriptsToActionsID is the ID of the ScriptsToActions migration +const ScriptsToActionsID = "scripts-to-actions" + +// ScriptsToActions migrates scripts to actions +type ScriptsToActions struct{} + +// String returns the name of the migration +func (ScriptsToActions) String() string { + return ScriptsToActionsID +} + +// Clear the deprecated scripts. +func (ScriptsToActions) Clear(mc types.ZarfComponent) types.ZarfComponent { + mc.DeprecatedScripts = types.DeprecatedZarfComponentScripts{} + return mc +} + +// Run coverts the deprecated scripts to the new actions // The following have no migration: // - Actions.Create.After // - Actions.Remove.* // - Actions.*.OnSuccess // - Actions.*.OnFailure // - Actions.*.*.Env -func migrateScriptsToActions(c types.ZarfComponent) (types.ZarfComponent, string) { +func (ScriptsToActions) Run(c types.ZarfComponent) (types.ZarfComponent, string) { var hasScripts bool // Convert a script configs to action defaults. diff --git a/src/test/upgrade/previously_built_test.go b/src/test/upgrade/previously_built_test.go index 179bad0432..7cd98c3644 100644 --- a/src/test/upgrade/previously_built_test.go +++ b/src/test/upgrade/previously_built_test.go @@ -6,9 +6,11 @@ package upgrade import ( "context" + "fmt" "path" "testing" + "github.com/defenseunicorns/zarf/src/config" "github.com/defenseunicorns/zarf/src/pkg/utils/exec" test "github.com/defenseunicorns/zarf/src/test" "github.com/stretchr/testify/require" @@ -45,7 +47,7 @@ func TestPreviouslyBuiltZarfPackage(t *testing.T) { require.Equal(t, zarfGitServerSecret, podinfoGitServerSecret, "the zarf git server secret and podinfo-upgrade git server secret did not match") // We also expect a 6.3.4 package to have been previously built - previouslyBuiltPackage := "../../../zarf-package-test-upgrade-package-amd64-6.3.4.tar.zst" + previouslyBuiltPackage := fmt.Sprintf("../../../zarf-package-test-upgrade-package-%s-6.3.4.tar.zst", config.GetArch()) // Deploy the package. zarfDeployArgs := []string{"package", "deploy", previouslyBuiltPackage, "--confirm"} @@ -65,7 +67,7 @@ func TestPreviouslyBuiltZarfPackage(t *testing.T) { // We also want to build a new package. stdOut, stdErr, err = zarf("package", "create", "../../../src/test/upgrade", "--set", "PODINFO_VERSION=6.3.5", "--confirm") require.NoError(t, err, stdOut, stdErr) - newlyBuiltPackage := "zarf-package-test-upgrade-package-amd64-6.3.5.tar.zst" + newlyBuiltPackage := fmt.Sprintf("zarf-package-test-upgrade-package-%s-6.3.5.tar.zst", config.GetArch()) // Deploy the package. stdOut, stdErr, err = zarf("package", "deploy", newlyBuiltPackage, "--confirm") diff --git a/src/types/component.go b/src/types/component.go index 3c4ffa21c4..9f9d9ae200 100644 --- a/src/types/component.go +++ b/src/types/component.go @@ -5,6 +5,8 @@ package types import ( + "slices" + "github.com/defenseunicorns/zarf/src/pkg/utils/exec" "github.com/defenseunicorns/zarf/src/pkg/variables" "github.com/defenseunicorns/zarf/src/types/extensions" @@ -82,11 +84,21 @@ func (c ZarfComponent) RequiresCluster() bool { } // IsRequired returns if the component is required or not. -func (c ZarfComponent) IsRequired() bool { +// +// If the `Required` field is set, it will return that value. +// +// If the `DefaultRequired` feature flag is present, it will return true. +// +// Otherwise, it will return false. +func (c ZarfComponent) IsRequired(ff []FeatureFlag) bool { if c.Required != nil { return *c.Required } + if slices.Contains(ff, DefaultRequired) { + return true + } + return false } diff --git a/src/types/feature_flags.go b/src/types/feature_flags.go new file mode 100644 index 0000000000..0c3acd5fd7 --- /dev/null +++ b/src/types/feature_flags.go @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package types contains all the types used by Zarf. +package types + +// FeatureFlag is an enum of the different feature flags that can be set on a Zarf package. +type FeatureFlag string + +const ( + // DefaultRequired changes the default state for all components in a package to be required. + DefaultRequired FeatureFlag = "default-required" +) + +// AllFeatureFlags returns a list of all available feature flags. +func AllFeatureFlags() []FeatureFlag { + return []FeatureFlag{ + DefaultRequired, + } +} diff --git a/src/types/package.go b/src/types/package.go index 4f3f222fb0..e2a75aa5eb 100644 --- a/src/types/package.go +++ b/src/types/package.go @@ -43,19 +43,20 @@ func (pkg ZarfPackage) IsSBOMAble() bool { // ZarfMetadata lists information about the current ZarfPackage. type ZarfMetadata struct { - Name string `json:"name" jsonschema:"description=Name to identify this Zarf package,pattern=^[a-z0-9\\-]*[a-z0-9]$"` - Description string `json:"description,omitempty" jsonschema:"description=Additional information about this package"` - Version string `json:"version,omitempty" jsonschema:"description=Generic string set by a package author to track the package version (Note: ZarfInitConfigs will always be versioned to the CLIVersion they were created with)"` - URL string `json:"url,omitempty" jsonschema:"description=Link to package information when online"` - Image string `json:"image,omitempty" jsonschema:"description=An image URL to embed in this package (Reserved for future use in Zarf UI)"` - Uncompressed bool `json:"uncompressed,omitempty" jsonschema:"description=Disable compression of this package"` - Architecture string `json:"architecture,omitempty" jsonschema:"description=The target cluster architecture for this package,example=arm64,example=amd64"` - YOLO bool `json:"yolo,omitempty" jsonschema:"description=Yaml OnLy Online (YOLO): True enables deploying a Zarf package without first running zarf init against the cluster. This is ideal for connected environments where you want to use existing VCS and container registries."` - Authors string `json:"authors,omitempty" jsonschema:"description=Comma-separated list of package authors (including contact info),example=Doug <hello@defenseunicorns.com>, Pepr <hello@defenseunicorns.com>"` - Documentation string `json:"documentation,omitempty" jsonschema:"description=Link to package documentation when online"` - Source string `json:"source,omitempty" jsonschema:"description=Link to package source code when online"` - Vendor string `json:"vendor,omitempty" jsonschema_description:"Name of the distributing entity, organization or individual."` - AggregateChecksum string `json:"aggregateChecksum,omitempty" jsonschema:"description=Checksum of a checksums.txt file that contains checksums all the layers within the package."` + Name string `json:"name" jsonschema:"description=Name to identify this Zarf package,pattern=^[a-z0-9\\-]*[a-z0-9]$"` + Description string `json:"description,omitempty" jsonschema:"description=Additional information about this package"` + Version string `json:"version,omitempty" jsonschema:"description=Generic string set by a package author to track the package version (Note: ZarfInitConfigs will always be versioned to the CLIVersion they were created with)"` + URL string `json:"url,omitempty" jsonschema:"description=Link to package information when online"` + Image string `json:"image,omitempty" jsonschema:"description=An image URL to embed in this package (Reserved for future use in Zarf UI)"` + Uncompressed bool `json:"uncompressed,omitempty" jsonschema:"description=Disable compression of this package"` + Architecture string `json:"architecture,omitempty" jsonschema:"description=The target cluster architecture for this package,example=arm64,example=amd64"` + YOLO bool `json:"yolo,omitempty" jsonschema:"description=Yaml OnLy Online (YOLO): True enables deploying a Zarf package without first running zarf init against the cluster. This is ideal for connected environments where you want to use existing VCS and container registries."` + Authors string `json:"authors,omitempty" jsonschema:"description=Comma-separated list of package authors (including contact info),example=Doug <hello@defenseunicorns.com>, Pepr <hello@defenseunicorns.com>"` + Documentation string `json:"documentation,omitempty" jsonschema:"description=Link to package documentation when online"` + Source string `json:"source,omitempty" jsonschema:"description=Link to package source code when online"` + Vendor string `json:"vendor,omitempty" jsonschema_description:"Name of the distributing entity, organization or individual."` + AggregateChecksum string `json:"aggregateChecksum,omitempty" jsonschema:"description=Checksum of a checksums.txt file that contains checksums all the layers within the package."` + Features []FeatureFlag `json:"features,omitempty" jsonschema:"description=List of feature flags to enable for this package"` } // ZarfBuildData is written during the packager.Create() operation to track details of the created package. diff --git a/src/types/validate.go b/src/types/validate.go index fbef1dab65..12bf3c4fe2 100644 --- a/src/types/validate.go +++ b/src/types/validate.go @@ -103,7 +103,7 @@ func (pkg ZarfPackage) Validate() error { return fmt.Errorf(lang.PkgValidateErrComponentLocalOS, component.Name, component.Only.LocalOS, supportedOS) } - if component.IsRequired() { + if component.IsRequired(pkg.Metadata.Features) { if component.Default { return fmt.Errorf(lang.PkgValidateErrComponentReqDefault, component.Name) } diff --git a/zarf.schema.json b/zarf.schema.json index 27a8a89a4c..52902c2ab4 100644 --- a/zarf.schema.json +++ b/zarf.schema.json @@ -1060,6 +1060,13 @@ "aggregateChecksum": { "type": "string", "description": "Checksum of a checksums.txt file that contains checksums all the layers within the package." + }, + "features": { + "items": { + "type": "string" + }, + "type": "array", + "description": "List of feature flags to enable for this package" } }, "additionalProperties": false, diff --git a/zarf.yaml b/zarf.yaml index 859813fde5..e79b4493e2 100644 --- a/zarf.yaml +++ b/zarf.yaml @@ -2,42 +2,43 @@ kind: ZarfInitConfig metadata: name: init description: Used to establish a new Zarf cluster + features: + - default-required components: - name: k3s + required: false import: path: packages/distros/k3s # This package moves the injector & registries binaries - name: zarf-injector - required: true import: path: packages/zarf-registry # Creates the temporary seed-registry - name: zarf-seed-registry - required: true import: path: packages/zarf-registry # Creates the permanent registry - name: zarf-registry - required: true import: path: packages/zarf-registry # Creates the pod+git mutating webhook - name: zarf-agent - required: true import: path: packages/zarf-agent # (Optional) Adds logging to the cluster - name: logging + required: false import: path: packages/logging-pgl # (Optional) Adds a git server to the cluster - name: git-server + required: false import: path: packages/gitea