diff --git a/site/src/content/docs/commands/zarf_dev_lint.md b/site/src/content/docs/commands/zarf_dev_lint.md index 499020d0c6..07c08e8c8f 100644 --- a/site/src/content/docs/commands/zarf_dev_lint.md +++ b/site/src/content/docs/commands/zarf_dev_lint.md @@ -21,6 +21,7 @@ zarf dev lint [ DIRECTORY ] [flags] ### Options ``` + --all-variants Lints all package components, regardless of flavor. NOTE: This will disable checks for component name uniqueness. -f, --flavor string The flavor of components to include in the resulting package (i.e. have a matching or empty "only.flavor" key) -h, --help help for lint --set stringToString Specify package templates to set on the command line (KEY=value) (default []) diff --git a/src/cmd/dev.go b/src/cmd/dev.go index f040b68cc9..9f4dc1cc43 100644 --- a/src/cmd/dev.go +++ b/src/cmd/dev.go @@ -1067,8 +1067,9 @@ func (o *devGenerateConfigOptions) run(_ *cobra.Command, args []string) error { } type devLintOptions struct { - setPkgTmpl map[string]string - flavor string + setPkgTmpl map[string]string + flavor string + allVariants bool } func newDevLintCommand(v *viper.Viper) *cobra.Command { @@ -1085,6 +1086,9 @@ func newDevLintCommand(v *viper.Viper) *cobra.Command { cmd.Flags().StringToStringVar(&o.setPkgTmpl, "set", v.GetStringMapString(VPkgCreateSet), lang.CmdPackageCreateFlagSetPkgTmpl) cmd.Flags().StringVarP(&o.flavor, "flavor", "f", v.GetString(VPkgCreateFlavor), lang.CmdPackageCreateFlagFlavor) + cmd.Flags().BoolVar(&o.allVariants, "all-variants", false, lang.CmdDevLintFlagAllVariants) + + cmd.MarkFlagsMutuallyExclusive("flavor", "all-variants") return cmd } @@ -1104,6 +1108,7 @@ func (o *devLintOptions) run(cmd *cobra.Command, args []string) error { } err = packager.Lint(ctx, basePath, packager.LintOptions{ Flavor: o.flavor, + AllVariants: o.allVariants, SetVariables: o.setPkgTmpl, CachePath: cachePath, RemoteOptions: defaultRemoteOptions(), diff --git a/src/config/lang/english.go b/src/config/lang/english.go index 9f82b07170..6fac93fdf0 100644 --- a/src/config/lang/english.go +++ b/src/config/lang/english.go @@ -544,8 +544,9 @@ $ zarf package pull oci://ghcr.io/zarf-dev/packages/dos-games:1.3.0 -a skeleton` CmdDevFlagGenerateSchemaUpdate = "Update the existing schema. Formatting such as ordering and newlines may change." CmdDevFlagGenerateSchemaDeleteNotFound = "Remove existing schema keys when they are not found in the mapped values" - CmdDevLintShort = "Lints the given package for valid schema and recommended practices" - CmdDevLintLong = "Verifies the package schema, checks if any variables won't be evaluated, and checks for unpinned images/repos/files" + CmdDevLintShort = "Lints the given package for valid schema and recommended practices" + CmdDevLintLong = "Verifies the package schema, checks if any variables won't be evaluated, and checks for unpinned images/repos/files" + CmdDevLintFlagAllVariants = "Lints all package components, regardless of flavor. NOTE: This will disable checks for component name uniqueness." // zarf tools CmdToolsShort = "Collection of additional tools to make airgap easier" diff --git a/src/internal/api/v1alpha1/validate.go b/src/internal/api/v1alpha1/validate.go index 911008e2a8..42ef85fcd3 100644 --- a/src/internal/api/v1alpha1/validate.go +++ b/src/internal/api/v1alpha1/validate.go @@ -7,6 +7,7 @@ package v1alpha1 import ( "errors" "fmt" + "slices" "strings" "github.com/zarf-dev/zarf/src/api/v1alpha1" @@ -51,8 +52,13 @@ const ( PkgValidateErrActionTemplateOnCreate = "templating is not supported in onCreate actions" ) +// ValidateOpts governs what validation checks are run in ValidatePackage +type ValidateOpts struct { + SkipComponentNameUniquenessValidation bool +} + // ValidatePackage runs all validation checks on the package. -func ValidatePackage(pkg v1alpha1.ZarfPackage) error { +func ValidatePackage(pkg v1alpha1.ZarfPackage, opts ValidateOpts) error { var err error if len(pkg.Components) == 0 { err = errors.Join(err, errors.New(PkgValidateErrNoComponents)) @@ -65,7 +71,7 @@ func ValidatePackage(pkg v1alpha1.ZarfPackage) error { err = errors.Join(err, fmt.Errorf(PkgValidateErrConstant, varErr)) } } - uniqueComponentNames := make(map[string]bool) + uniqueComponentNames := make(map[string][]v1alpha1.ZarfComponentOnlyTarget) groupDefault := make(map[string]string) groupedComponents := make(map[string][]string) if pkg.Metadata.YOLO { @@ -85,11 +91,21 @@ func ValidatePackage(pkg v1alpha1.ZarfPackage) error { } } for _, component := range pkg.Components { + var duplicateOnly bool // ensure component name is unique if _, ok := uniqueComponentNames[component.Name]; ok { - err = errors.Join(err, fmt.Errorf(PkgValidateErrComponentNameNotUnique, component.Name)) + // only check if only block is duplicated if we're skipping name uniqueness checks + if opts.SkipComponentNameUniquenessValidation { + duplicateOnly = slices.ContainsFunc(uniqueComponentNames[component.Name], func(o v1alpha1.ZarfComponentOnlyTarget) bool { + return onlyTargetsEqual(o, component.Only) + }) + } + if !opts.SkipComponentNameUniquenessValidation || duplicateOnly { + err = errors.Join(err, fmt.Errorf(PkgValidateErrComponentNameNotUnique, component.Name)) + } } - uniqueComponentNames[component.Name] = true + + uniqueComponentNames[component.Name] = append(uniqueComponentNames[component.Name], component.Only) if component.IsRequired() { if component.Default { err = errors.Join(err, fmt.Errorf(PkgValidateErrComponentReqDefault, component.Name)) @@ -334,3 +350,7 @@ func validateManifest(manifest v1alpha1.ZarfManifest) error { return err } + +func onlyTargetsEqual(a, b v1alpha1.ZarfComponentOnlyTarget) bool { + return a.LocalOS == b.LocalOS && a.Flavor == b.Flavor && a.Cluster.Architecture == b.Cluster.Architecture && (slices.Equal(a.Cluster.Distros, b.Cluster.Distros)) +} diff --git a/src/internal/api/v1alpha1/validate_test.go b/src/internal/api/v1alpha1/validate_test.go index 5a274629d1..472f4447d3 100644 --- a/src/internal/api/v1alpha1/validate_test.go +++ b/src/internal/api/v1alpha1/validate_test.go @@ -17,9 +17,10 @@ import ( func TestZarfPackageValidate(t *testing.T) { t.Parallel() tests := []struct { - name string - pkg v1alpha1.ZarfPackage - expectedErrs []string + name string + pkg v1alpha1.ZarfPackage + expectedErrs []string + validateOptions ValidateOpts }{ { name: "valid package", @@ -138,12 +139,134 @@ func TestZarfPackageValidate(t *testing.T) { PkgValidateErrYOLONoDistro, }, }, + { + name: "duplicate component names", + pkg: v1alpha1.ZarfPackage{ + Kind: v1alpha1.ZarfPackageConfig, + Metadata: v1alpha1.ZarfMetadata{ + Name: "duplicate-component-name-pacakage", + }, + Components: []v1alpha1.ZarfComponent{ + { + Name: "component1", + }, + { + Name: "component1", + }, + }, + }, + expectedErrs: []string{ + fmt.Sprintf(PkgValidateErrComponentNameNotUnique, "component1"), + }, + }, + { + name: "duplicate component names, skip component name uniqueness validation", + pkg: v1alpha1.ZarfPackage{ + Kind: v1alpha1.ZarfPackageConfig, + Metadata: v1alpha1.ZarfMetadata{ + Name: "duplicate-component-name-pacakage", + }, + Components: []v1alpha1.ZarfComponent{ + { + Name: "component1", + Only: v1alpha1.ZarfComponentOnlyTarget{ + Flavor: "strawberry", + LocalOS: "darwin", + Cluster: v1alpha1.ZarfComponentOnlyCluster{ + Architecture: "arm64", + Distros: []string{ + "eks", + "rke2", + }, + }, + }, + }, + { + Name: "component1", + Only: v1alpha1.ZarfComponentOnlyTarget{ + Flavor: "blueberry", + LocalOS: "darwin", + Cluster: v1alpha1.ZarfComponentOnlyCluster{ + Architecture: "arm64", + Distros: []string{ + "eks", + "rke2", + }, + }, + }, + }, + }, + }, + expectedErrs: nil, + validateOptions: ValidateOpts{ + SkipComponentNameUniquenessValidation: true, + }, + }, + { + name: "duplicate component names, skip component name uniqueness validation, only blocks are the same", + pkg: v1alpha1.ZarfPackage{ + Kind: v1alpha1.ZarfPackageConfig, + Metadata: v1alpha1.ZarfMetadata{ + Name: "duplicate-component-name-pacakage-with-duplicate-only", + }, + Components: []v1alpha1.ZarfComponent{ + { + Name: "component2", + Only: v1alpha1.ZarfComponentOnlyTarget{ + Flavor: "test", + LocalOS: "darwin", + Cluster: v1alpha1.ZarfComponentOnlyCluster{ + Architecture: "arm64", + Distros: []string{ + "eks", + "rke2", + }, + }, + }, + }, + { + Name: "component2", + Only: v1alpha1.ZarfComponentOnlyTarget{ + Flavor: "different", + LocalOS: "darwin", + Cluster: v1alpha1.ZarfComponentOnlyCluster{ + Architecture: "arm64", + Distros: []string{ + "eks", + "rke2", + }, + }, + }, + }, + { + Name: "component2", + Only: v1alpha1.ZarfComponentOnlyTarget{ + Flavor: "test", + LocalOS: "darwin", + Cluster: v1alpha1.ZarfComponentOnlyCluster{ + Architecture: "arm64", + Distros: []string{ + "eks", + "rke2", + }, + }, + }, + }, + }, + }, + expectedErrs: []string{ + fmt.Sprintf(PkgValidateErrComponentNameNotUnique, "component2"), + }, + validateOptions: ValidateOpts{ + SkipComponentNameUniquenessValidation: true, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - err := ValidatePackage(tt.pkg) + err := ValidatePackage(tt.pkg, tt.validateOptions) if tt.expectedErrs == nil { require.NoError(t, err) return diff --git a/src/pkg/packager/generate.go b/src/pkg/packager/generate.go index 88a87a3410..d85811585b 100644 --- a/src/pkg/packager/generate.go +++ b/src/pkg/packager/generate.go @@ -94,7 +94,9 @@ func Generate(ctx context.Context, packageName, url, version string, opts Genera pkg.Components[i].Images = append(pkg.Components[i].Images, imageScan.CosignArtifacts...) } - if err := internalv1alpha1.ValidatePackage(pkg); err != nil { + validateOpts := internalv1alpha1.ValidateOpts{} + + if err := internalv1alpha1.ValidatePackage(pkg, validateOpts); err != nil { return v1alpha1.ZarfPackage{}, err } return pkg, nil diff --git a/src/pkg/packager/lint.go b/src/pkg/packager/lint.go index 739553b1a1..02b74821eb 100644 --- a/src/pkg/packager/lint.go +++ b/src/pkg/packager/lint.go @@ -17,6 +17,7 @@ import ( type LintOptions struct { SetVariables map[string]string Flavor string + AllVariants bool CachePath string types.RemoteOptions } @@ -35,6 +36,7 @@ func Lint(ctx context.Context, packagePath string, opts LintOptions) error { loadOpts := load.DefinitionOptions{ Flavor: opts.Flavor, + AllVariants: opts.AllVariants, SetVariables: opts.SetVariables, CachePath: opts.CachePath, IsInteractive: false, diff --git a/src/pkg/packager/lint_test.go b/src/pkg/packager/lint_test.go index 4e4dc74a69..d83910eac9 100644 --- a/src/pkg/packager/lint_test.go +++ b/src/pkg/packager/lint_test.go @@ -92,6 +92,57 @@ func TestLintPackageWithImports(t *testing.T) { }, }, }, + { + name: "all-variants test", + path: filepath.Join("testdata", "lint-with-imports", "all-variants"), + opts: LintOptions{ + AllVariants: true, + }, + findings: []lint.PackageFinding{ + { + YqPath: ".components.[0].images.[0]", + Description: "Image not pinned with digest", + Item: "image-in-first-flavor-component:1.0.0", + Severity: lint.SevWarn, + }, + { + YqPath: ".components.[1].images.[0]", + Description: "Image not pinned with digest", + Item: "image-in-second-flavor-component:1.0.0", + Severity: lint.SevWarn, + }, + { + YqPath: ".components.[2].images.[0]", + Description: "Image not pinned with digest", + Item: "busybox:0.0.1", + Severity: lint.SevWarn, + }, + { + YqPath: ".components.[0].images.[0]", + Description: "Image reference does not specify a registry domain", + Item: "image-in-first-flavor-component:1.0.0", + Severity: lint.SevWarn, + }, + { + YqPath: ".components.[1].images.[0]", + Description: "Image reference does not specify a registry domain", + Item: "image-in-second-flavor-component:1.0.0", + Severity: lint.SevWarn, + }, + { + YqPath: ".components.[2].images.[0]", + Description: "Image reference does not specify a registry domain", + Item: "busybox:0.0.1", + Severity: lint.SevWarn, + }, + { + YqPath: ".components.[2].images.[1]", + Description: "Image reference does not specify a registry domain", + Item: "busybox@sha256:3fbc632167424a6d997e74f52b878d7cc478225cffac6bc977eedfe51c7f4e79", + Severity: lint.SevWarn, + }, + }, + }, } for _, tc := range testCases { diff --git a/src/pkg/packager/load/import.go b/src/pkg/packager/load/import.go index b5a6c37f13..85d0e7da3e 100644 --- a/src/pkg/packager/load/import.go +++ b/src/pkg/packager/load/import.go @@ -55,7 +55,7 @@ func getComponentToImportName(component v1alpha1.ZarfComponent) string { return component.Name } -func resolveImports(ctx context.Context, pkg v1alpha1.ZarfPackage, packagePath, arch, flavor string, importStack []string, cachePath string, skipVersionCheck bool, remoteOptions types.RemoteOptions) (v1alpha1.ZarfPackage, []string, error) { +func resolveImports(ctx context.Context, pkg v1alpha1.ZarfPackage, packagePath, arch, flavor string, importStack []string, cachePath string, allVariants, skipVersionCheck bool, remoteOptions types.RemoteOptions) (v1alpha1.ZarfPackage, []string, error) { l := logger.From(ctx) start := time.Now() @@ -86,7 +86,7 @@ func resolveImports(ctx context.Context, pkg v1alpha1.ZarfPackage, packagePath, components := []v1alpha1.ZarfComponent{} for _, component := range pkg.Components { - if !compatibleComponent(component, arch, flavor) { + if !compatibleComponent(component, arch, flavor, allVariants) { continue } @@ -130,7 +130,7 @@ func resolveImports(ctx context.Context, pkg v1alpha1.ZarfPackage, packagePath, } } importedPkg.Components = relevantComponents - importedPkg, innerSchemas, err = resolveImports(ctx, importedPkg, importPkgPath.ManifestFile, arch, flavor, importStack, cachePath, skipVersionCheck, remoteOptions) + importedPkg, innerSchemas, err = resolveImports(ctx, importedPkg, importPkgPath.ManifestFile, arch, flavor, importStack, cachePath, allVariants, skipVersionCheck, remoteOptions) if err != nil { return v1alpha1.ZarfPackage{}, nil, err } @@ -175,22 +175,19 @@ func resolveImports(ctx context.Context, pkg v1alpha1.ZarfPackage, packagePath, name := getComponentToImportName(component) found := []v1alpha1.ZarfComponent{} for _, component := range importedPkg.Components { - if component.Name == name && compatibleComponent(component, arch, flavor) { + if component.Name == name && compatibleComponent(component, arch, flavor, allVariants) { found = append(found, component) } } if len(found) == 0 { return v1alpha1.ZarfPackage{}, nil, fmt.Errorf("no compatible component named %s found", name) - } else if len(found) > 1 { + } else if len(found) > 1 && !allVariants { return v1alpha1.ZarfPackage{}, nil, fmt.Errorf("multiple components named %s found", name) } - importedComponent := found[0] - importPath, err := fetchOCISkeleton(ctx, component, pkgPath.BaseDir, cachePath, remoteOptions) if err != nil { return v1alpha1.ZarfPackage{}, nil, err } - // this is a special case for paths and imports where we do not want to join BaseDir and importPath // we check that the path is valid but ensure the value remains relative for fixing fileInfo, err := os.Stat(filepath.Join(pkgPath.BaseDir, importPath)) @@ -200,16 +197,18 @@ func resolveImports(ctx context.Context, pkg v1alpha1.ZarfPackage, packagePath, if !fileInfo.IsDir() { importPath = filepath.Dir(importPath) } - importedComponent = fixPaths(importedComponent, importPath, pkgPath.BaseDir) - composed, err := overrideMetadata(importedComponent, component) - if err != nil { - return v1alpha1.ZarfPackage{}, nil, err - } - composed = overrideDeprecated(composed, component) - composed = overrideActions(composed, component) - composed = overrideResources(composed, component) + for i := range found { + importedComponent := fixPaths(found[i], importPath, pkgPath.BaseDir) + composed, err := overrideMetadata(importedComponent, component) + if err != nil { + return v1alpha1.ZarfPackage{}, nil, err + } + composed = overrideDeprecated(composed, component) + composed = overrideActions(composed, component) + composed = overrideResources(composed, component) - components = append(components, composed) + components = append(components, composed) + } variables = append(variables, importedPkg.Variables...) constants = append(constants, importedPkg.Constants...) for _, v := range importedPkg.Values.Files { @@ -299,9 +298,9 @@ func validateComponentCompose(c v1alpha1.ZarfComponent) error { return errors.Join(errs...) } -func compatibleComponent(c v1alpha1.ZarfComponent, arch, flavor string) bool { - satisfiesArch := c.Only.Cluster.Architecture == "" || c.Only.Cluster.Architecture == arch - satisfiesFlavor := c.Only.Flavor == "" || c.Only.Flavor == flavor +func compatibleComponent(c v1alpha1.ZarfComponent, arch, flavor string, allVariants bool) bool { + satisfiesArch := c.Only.Cluster.Architecture == "" || c.Only.Cluster.Architecture == arch || allVariants + satisfiesFlavor := c.Only.Flavor == "" || c.Only.Flavor == flavor || allVariants return satisfiesArch && satisfiesFlavor } diff --git a/src/pkg/packager/load/import_test.go b/src/pkg/packager/load/import_test.go index b0df766a6e..e496c7949d 100644 --- a/src/pkg/packager/load/import_test.go +++ b/src/pkg/packager/load/import_test.go @@ -29,7 +29,7 @@ func TestResolveImportsCircular(t *testing.T) { pkg, err := pkgcfg.Parse(ctx, b) require.NoError(t, err) - _, _, err = resolveImports(ctx, pkg, "./testdata/import/circular/first", "", "", []string{}, "", false, types.RemoteOptions{}) + _, _, err = resolveImports(ctx, pkg, "./testdata/import/circular/first", "", "", []string{}, "", false, false, types.RemoteOptions{}) require.EqualError(t, err, "package testdata/import/circular/second imported in cycle by testdata/import/circular/third in component component") } @@ -42,6 +42,7 @@ func TestResolveImports(t *testing.T) { path string flavor string expectedChecksum string + allVariants bool }{ { name: "two zarf.yaml files import each other", @@ -104,6 +105,12 @@ func TestResolveImports(t *testing.T) { path: "./testdata/import/archives", expectedChecksum: "9601cb578d72727bba116d008a23f63ac6dd40c3a685e1d790d376469792db5a", }, + { + name: "all variants are included", + path: "./testdata/import/all-variants", + expectedChecksum: "7b9b0d81444cef4db9e2b57f9620e6db4bedc659e08faaa32a3b4b061b176e16", + allVariants: true, + }, } for _, tc := range testCases { @@ -115,7 +122,7 @@ func TestResolveImports(t *testing.T) { pkg, err := pkgcfg.Parse(ctx, b) require.NoError(t, err) - resolvedPkg, _, err := resolveImports(ctx, pkg, tc.path, "", tc.flavor, []string{}, "", false, types.RemoteOptions{}) + resolvedPkg, _, err := resolveImports(ctx, pkg, tc.path, "", tc.flavor, []string{}, "", tc.allVariants, false, types.RemoteOptions{}) require.NoError(t, err) b, err = os.ReadFile(filepath.Join(tc.path, "expected.yaml")) @@ -150,7 +157,7 @@ func TestResolveImportsDedupNormalization(t *testing.T) { // Reuse an existing fixture's directory only as the on-disk anchor — resolveImports // stats the path but does not re-parse zarf.yaml when pkg is passed in. resolved, _, err := resolveImports(ctx, pkg, "./testdata/import/values/duplicate-consecutive", - "", "", []string{}, "", false, types.RemoteOptions{}) + "", "", []string{}, "", false, false, types.RemoteOptions{}) require.NoError(t, err) require.Equal(t, []string{"parent-values.yaml"}, resolved.Values.Files) } @@ -245,7 +252,7 @@ func TestResolveImportsValueMerge(t *testing.T) { pkg, err := pkgcfg.Parse(ctx, b) require.NoError(t, err) - resolved, _, err := resolveImports(ctx, pkg, tc.path, "", "", []string{}, "", false, types.RemoteOptions{}) + resolved, _, err := resolveImports(ctx, pkg, tc.path, "", "", []string{}, "", false, false, types.RemoteOptions{}) require.NoError(t, err) absPaths := make([]string, len(resolved.Values.Files)) @@ -303,7 +310,7 @@ func TestResolveImportsSchemaCollection(t *testing.T) { pkg, err := pkgcfg.Parse(ctx, b) require.NoError(t, err) - resolved, importedSchemas, err := resolveImports(ctx, pkg, tc.path, "", "", []string{}, "", false, types.RemoteOptions{}) + resolved, importedSchemas, err := resolveImports(ctx, pkg, tc.path, "", "", []string{}, "", false, false, types.RemoteOptions{}) require.NoError(t, err) require.Equal(t, tc.expectedSchemas, importedSchemas) @@ -439,6 +446,7 @@ func TestCompatibleComponent(t *testing.T) { component v1alpha1.ZarfComponent arch string flavor string + allVariants bool expectedResult bool }{ { @@ -516,7 +524,7 @@ func TestCompatibleComponent(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - result := compatibleComponent(tt.component, tt.arch, tt.flavor) + result := compatibleComponent(tt.component, tt.arch, tt.flavor, tt.allVariants) require.Equal(t, tt.expectedResult, result) }) } diff --git a/src/pkg/packager/load/load.go b/src/pkg/packager/load/load.go index 93d3fb806e..a67fa6fc4c 100644 --- a/src/pkg/packager/load/load.go +++ b/src/pkg/packager/load/load.go @@ -28,7 +28,9 @@ import ( // DefinitionOptions are the optional parameters to load.PackageDefinition type DefinitionOptions struct { - Flavor string + Flavor string + // All variants will ignore Flavor & architecture and will return all components. Mutually exclusive with flavor. + AllVariants bool SetVariables map[string]string // SkipRequiredValues ignores values schema validation errors when a "required" field is empty. Used when a package // value should be supplied at deploy-time and doesn't have a default set in the package values. @@ -59,9 +61,14 @@ func PackageDefinition(ctx context.Context, packagePath string, opts DefinitionO l.Debug("start layout.LoadPackage", "path", packagePath, "flavor", opts.Flavor, + "allVariants", opts.AllVariants, "setVariables", opts.SetVariables, ) + if opts.Flavor != "" && opts.AllVariants { + return DefinedPackage{}, fmt.Errorf("only one of Flavor or AllVariants can be set") + } + pkgPath, err := layout.ResolvePackagePath(packagePath) if err != nil { return DefinedPackage{}, err @@ -81,7 +88,7 @@ func PackageDefinition(ctx context.Context, packagePath string, opts DefinitionO return DefinedPackage{}, err } var importedSchemas []string - pkg, importedSchemas, err = resolveImports(ctx, pkg, pkgPath.ManifestFile, pkg.Metadata.Architecture, opts.Flavor, []string{}, opts.CachePath, opts.SkipVersionCheck, opts.RemoteOptions) + pkg, importedSchemas, err = resolveImports(ctx, pkg, pkgPath.ManifestFile, pkg.Metadata.Architecture, opts.Flavor, []string{}, opts.CachePath, opts.AllVariants, opts.SkipVersionCheck, opts.RemoteOptions) if err != nil { return DefinedPackage{}, err } @@ -97,7 +104,7 @@ func PackageDefinition(ctx context.Context, packagePath string, opts DefinitionO return DefinedPackage{}, err } } - err = validate(ctx, pkg, pkgPath.ManifestFile, opts.SetVariables, opts.Flavor, opts.SkipRequiredValues, opts.SkipValuesSchemaValidation) + err = validate(ctx, pkg, pkgPath.ManifestFile, opts.SetVariables, opts.Flavor, opts.AllVariants, opts.SkipRequiredValues, opts.SkipValuesSchemaValidation) if err != nil { return DefinedPackage{}, err } @@ -105,22 +112,29 @@ func PackageDefinition(ctx context.Context, packagePath string, opts DefinitionO return DefinedPackage{Pkg: pkg, ImportedSchemas: importedSchemas}, nil } -func validate(ctx context.Context, pkg v1alpha1.ZarfPackage, packagePath string, setVariables map[string]string, flavor string, skipRequiredValues bool, skipSchemaValidation bool) error { +func validate(ctx context.Context, pkg v1alpha1.ZarfPackage, packagePath string, setVariables map[string]string, flavor string, allVariants, skipRequiredValues bool, skipSchemaValidation bool) error { l := logger.From(ctx) start := time.Now() l.Debug("start layout.Validate", "pkg", pkg.Metadata.Name, "packagePath", packagePath, "flavor", flavor, + "allVariants", allVariants, "setVariables", setVariables, ) - if !hasFlavoredComponent(pkg, flavor) { + if !hasFlavoredComponent(pkg, flavor) && !allVariants { l.Warn("flavor not used in package", "flavor", flavor) } - if err := internalv1alpha1.ValidatePackage(pkg); err != nil { + + validationOpts := internalv1alpha1.ValidateOpts{ + SkipComponentNameUniquenessValidation: allVariants, + } + + if err := internalv1alpha1.ValidatePackage(pkg, validationOpts); err != nil { return fmt.Errorf("package validation failed: %w", err) } + findings, err := lint.ValidatePackageSchemaAtPath(packagePath, setVariables) if err != nil { return fmt.Errorf("unable to check schema: %w", err) diff --git a/src/pkg/packager/load/load_test.go b/src/pkg/packager/load/load_test.go index 39b7179a1d..ec516e13c7 100644 --- a/src/pkg/packager/load/load_test.go +++ b/src/pkg/packager/load/load_test.go @@ -16,22 +16,38 @@ import ( "github.com/zarf-dev/zarf/src/test/testutil" ) -func TestLoadPackageWithFlavors(t *testing.T) { +func TestLoadPackage(t *testing.T) { t.Parallel() tests := []struct { name string flavor string + allVariants bool + packageDir string expectedErr string }{ { name: "when all components have a flavor, inputting no flavor should error", flavor: "", + packageDir: "package-with-flavors", expectedErr: fmt.Sprintf("package validation failed: %s", "package does not contain any compatible components"), }, { - name: "flavors work", - flavor: "cashew", + name: "when flavor and allVariants are set, we should error", + flavor: "foo", + packageDir: "package-with-flavors", + allVariants: true, + expectedErr: "only one of Flavor or AllVariants can be set", + }, + { + name: "when allVariants is set for a package without flavors, we should not error", + packageDir: "package-without-flavors", + allVariants: true, + }, + { + name: "flavors work", + packageDir: "package-with-flavors", + flavor: "cashew", }, } @@ -39,9 +55,10 @@ func TestLoadPackageWithFlavors(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() opts := DefinitionOptions{ - Flavor: tt.flavor, + Flavor: tt.flavor, + AllVariants: tt.allVariants, } - _, err := PackageDefinition(context.Background(), filepath.Join("testdata", "package-with-flavors"), opts) + _, err := PackageDefinition(context.Background(), filepath.Join("testdata", tt.packageDir), opts) if tt.expectedErr != "" { require.ErrorContains(t, err, tt.expectedErr) return diff --git a/src/pkg/packager/load/testdata/import/all-variants/child/zarf.yaml b/src/pkg/packager/load/testdata/import/all-variants/child/zarf.yaml new file mode 100644 index 0000000000..328a24806b --- /dev/null +++ b/src/pkg/packager/load/testdata/import/all-variants/child/zarf.yaml @@ -0,0 +1,30 @@ +kind: ZarfPackageConfig +metadata: + name: example-package-flavors-child + +constants: + - name: TEST_CONST + value: "foo" +variables: + - name: TEST_VAR + +components: + - name: has-no-flavor + + - name: child-has-flavor + only: + flavor: pistachio + + - name: different-child-flavor + only: + flavor: seaweed + + - name: arches + only: + cluster: + architecture: amd64 + + - name: arches + only: + cluster: + architecture: arm64 diff --git a/src/pkg/packager/load/testdata/import/all-variants/expected.yaml b/src/pkg/packager/load/testdata/import/all-variants/expected.yaml new file mode 100644 index 0000000000..bca3fcf8b6 --- /dev/null +++ b/src/pkg/packager/load/testdata/import/all-variants/expected.yaml @@ -0,0 +1,33 @@ +kind: ZarfPackageConfig +metadata: + name: example-package-flavors +constants: + - name: TEST_CONST + value: "foo" +variables: + - name: TEST_VAR +components: + - name: has-flavor + description: this already has a flavor so it shouldn't get overwritten + only: + flavor: pistachio + - name: child-has-flavor + description: this doesn't have a flavor so it should get it's child's flavor + only: + flavor: pistachio + - name: has-different-flavor + description: should also be included when all-variants is true + only: + flavor: caramel + - name: child-has-different-flavor + description: ensure that all flavors are also included from child + only: + flavor: seaweed + - name: has-multiple-child-arches + only: + cluster: + architecture: amd64 + - name: has-multiple-child-arches + only: + cluster: + architecture: arm64 diff --git a/src/pkg/packager/load/testdata/import/all-variants/zarf.yaml b/src/pkg/packager/load/testdata/import/all-variants/zarf.yaml new file mode 100644 index 0000000000..39cbc621e5 --- /dev/null +++ b/src/pkg/packager/load/testdata/import/all-variants/zarf.yaml @@ -0,0 +1,33 @@ +kind: ZarfPackageConfig +metadata: + name: example-package-flavors + +components: + - name: has-flavor + description: this already has a flavor so it shouldn't get overwritten + import: + path: child + name: has-no-flavor + only: + flavor: pistachio + + - name: child-has-flavor + description: this doesn't have a flavor so it should get it's child's flavor + import: + path: child + + - name: has-different-flavor + description: should also be included when all-variants is true + only: + flavor: caramel + + - name: child-has-different-flavor + description: ensure that all flavors are also included from child + import: + path: child + name: different-child-flavor + + - name: has-multiple-child-arches + import: + path: child + name: arches diff --git a/src/pkg/packager/load/testdata/package-without-flavors/zarf.yaml b/src/pkg/packager/load/testdata/package-without-flavors/zarf.yaml new file mode 100644 index 0000000000..3595722a2b --- /dev/null +++ b/src/pkg/packager/load/testdata/package-without-flavors/zarf.yaml @@ -0,0 +1,5 @@ +kind: ZarfPackageConfig +metadata: + name: test +components: + - name: test-flavor diff --git a/src/pkg/packager/testdata/lint-with-imports/all-variants/linted-import/zarf.yaml b/src/pkg/packager/testdata/lint-with-imports/all-variants/linted-import/zarf.yaml new file mode 100644 index 0000000000..2ca94a520a --- /dev/null +++ b/src/pkg/packager/testdata/lint-with-imports/all-variants/linted-import/zarf.yaml @@ -0,0 +1,15 @@ +kind: ZarfPackageConfig +metadata: + name: linted-import + +components: + - name: dont-care + images: + - image-that-should-not-show-up-in-lint:unpinned + + - name: import-test + only: + flavor: third-flavor + images: + - busybox:0.0.1 + - busybox@sha256:3fbc632167424a6d997e74f52b878d7cc478225cffac6bc977eedfe51c7f4e79 diff --git a/src/pkg/packager/testdata/lint-with-imports/all-variants/zarf.yaml b/src/pkg/packager/testdata/lint-with-imports/all-variants/zarf.yaml new file mode 100644 index 0000000000..34d01d7d5e --- /dev/null +++ b/src/pkg/packager/testdata/lint-with-imports/all-variants/zarf.yaml @@ -0,0 +1,19 @@ +kind: ZarfPackageConfig +metadata: + name: all-variants-lint + +components: + - name: duplicate + only: + flavor: first-flavor + images: + - image-in-first-flavor-component:1.0.0 + - name: duplicate + only: + flavor: second-flavor + images: + - image-in-second-flavor-component:1.0.0 + + - name: import-test + import: + path: linted-import diff --git a/src/pkg/utils/oci_artifacts.go b/src/pkg/utils/oci_artifacts.go index e4078c9b42..b9a0ca3301 100644 --- a/src/pkg/utils/oci_artifacts.go +++ b/src/pkg/utils/oci_artifacts.go @@ -52,6 +52,7 @@ func GetCosignArtifacts(ctx context.Context, image string, client *auth.Client, // `crane` lookup that would otherwise happen in ociremote.SignatureTag and ociremote.AttestationTag digestRef, err := imageDigestRef(ctx, image, ref, client, plainHTTP) if err != nil { + l.Info("could not get digest reference for image", "image", image, "error", err) // If we can't get the digest reference, we can't get the cosign artifacts so log the error and skip it l.Debug("could not get digest reference for image", "image", image, "error", err) return nil, nil