diff --git a/src/config/lang/english.go b/src/config/lang/english.go index adae1f1a41..36112f99a9 100644 --- a/src/config/lang/english.go +++ b/src/config/lang/english.go @@ -695,7 +695,7 @@ const ( PkgValidateErrActionClusterNetwork = "a single wait action must contain only one of cluster or network" PkgValidateErrChart = "invalid chart definition: %w" PkgValidateErrChartName = "chart %q exceed the maximum length of %d characters" - PkgValidateErrChartNameMissing = "chart %q must include a name" + PkgValidateErrChartNameMissing = "chart must include a name" PkgValidateErrChartNameNotUnique = "chart name %q is not unique" PkgValidateErrChartNamespaceMissing = "chart %q must include a namespace" PkgValidateErrChartURLOrPath = "chart %q must have either a url or localPath" @@ -715,17 +715,17 @@ const ( PkgValidateErrManifest = "invalid manifest definition: %w" PkgValidateErrManifestFileOrKustomize = "manifest %q must have at least one file or kustomization" PkgValidateErrManifestNameLength = "manifest %q exceed the maximum length of %d characters" - PkgValidateErrManifestNameMissing = "manifest %q must include a name" + PkgValidateErrManifestNameMissing = "manifest must include a name" PkgValidateErrManifestNameNotUnique = "manifest name %q is not unique" PkgValidateErrName = "invalid package name: %w" PkgValidateErrPkgConstantName = "constant name %q must be all uppercase and contain no special characters except _" PkgValidateErrPkgConstantPattern = "provided value for constant %q does not match pattern %q" PkgValidateErrPkgName = "package name %q must be all lowercase and contain no special characters except '-' and cannot start with a '-'" PkgValidateErrVariable = "invalid package variable: %w" - PkgValidateErrYOLONoArch = "cluster architecture not allowed" - PkgValidateErrYOLONoDistro = "cluster distros not allowed" - PkgValidateErrYOLONoGit = "git repos not allowed" - PkgValidateErrYOLONoOCI = "OCI images not allowed" + PkgValidateErrYOLONoArch = "cluster architecture not allowed in YOLO" + PkgValidateErrYOLONoDistro = "cluster distros not allowed in YOLO" + PkgValidateErrYOLONoGit = "git repos not allowed in YOLO" + PkgValidateErrYOLONoOCI = "OCI images not allowed in YOLO" ) // Collection of reusable error messages. diff --git a/src/pkg/message/message.go b/src/pkg/message/message.go index c3eab711bf..9cafa0f1c1 100644 --- a/src/pkg/message/message.go +++ b/src/pkg/message/message.go @@ -273,7 +273,15 @@ func Paragraph(format string, a ...any) string { // Paragraphn formats text into an n column paragraph func Paragraphn(n int, format string, a ...any) string { - return pterm.DefaultParagraph.WithMaxWidth(n).Sprintf(format, a...) + // Split the text to keep pterm formatting but add newlines + lines := strings.Split(fmt.Sprintf(format, a...), "\n") + + formattedLines := make([]string, len(lines)) + for i, line := range lines { + formattedLines[i] = pterm.DefaultParagraph.WithMaxWidth(n).Sprintf(line) + } + + return strings.Join(formattedLines, "\n") } // PrintDiff prints the differences between a and b with a as original and b as new diff --git a/src/pkg/packager/composer/list.go b/src/pkg/packager/composer/list.go index 4f1a484ca3..c88a0bb019 100644 --- a/src/pkg/packager/composer/list.go +++ b/src/pkg/packager/composer/list.go @@ -142,7 +142,7 @@ func NewImportChain(head types.ZarfComponent, index int, originalPackageName, ar } // TODO: stuff like this should also happen in linting - if err := node.ZarfComponent.ValidateImportDefinition(); err != nil { + if err := node.ZarfComponent.Validate(); err != nil { return ic, err } diff --git a/src/pkg/packager/sources/new_test.go b/src/pkg/packager/sources/new_test.go index 91ea3e2c2f..ee770f51aa 100644 --- a/src/pkg/packager/sources/new_test.go +++ b/src/pkg/packager/sources/new_test.go @@ -150,8 +150,7 @@ func TestPackageSource(t *testing.T) { for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { - t.Parallel() - + // TODO once our messaging is thread safe, re-parallelize this test opts := &types.ZarfPackageOptions{ PackageSource: tt.src, Shasum: tt.shasum, diff --git a/src/types/component.go b/src/types/component.go index 3c4ffa21c4..a9997ac504 100644 --- a/src/types/component.go +++ b/src/types/component.go @@ -13,7 +13,7 @@ import ( // ZarfComponent is the primary functional grouping of assets to deploy by Zarf. type ZarfComponent struct { // Name is the unique identifier for this component - Name string `json:"name" jsonschema:"description=The name of the component,pattern=^[a-z0-9\\-]*[a-z0-9]$"` + Name string `json:"name" jsonschema:"description=The name of the component,pattern=^[a-z0-9][a-z0-9\\-]*$"` // Description is a message given to a user when deciding to enable this component or not Description string `json:"description,omitempty" jsonschema:"description=Message to include during package deploy describing the purpose of this component"` diff --git a/src/types/package.go b/src/types/package.go index 4f3f222fb0..99ac9ef008 100644 --- a/src/types/package.go +++ b/src/types/package.go @@ -43,7 +43,7 @@ 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]$"` + 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"` diff --git a/src/types/validate.go b/src/types/validate.go index fbef1dab65..fa5cfcbd87 100644 --- a/src/types/validate.go +++ b/src/types/validate.go @@ -40,27 +40,28 @@ func SupportedOS() []string { // Validate runs all validation checks on the package. func (pkg ZarfPackage) Validate() error { + var err error if pkg.Kind == ZarfInitConfig && pkg.Metadata.YOLO { - return fmt.Errorf(lang.PkgValidateErrInitNoYOLO) + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrInitNoYOLO)) } if !IsLowercaseNumberHyphenNoStartHyphen(pkg.Metadata.Name) { - return fmt.Errorf(lang.PkgValidateErrPkgName, pkg.Metadata.Name) + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrPkgName, pkg.Metadata.Name)) } if len(pkg.Components) == 0 { - return errors.New("package must have at least 1 component") + err = errors.Join(err, fmt.Errorf("package must have at least 1 component")) } for _, variable := range pkg.Variables { - if err := variable.Validate(); err != nil { - return fmt.Errorf(lang.PkgValidateErrVariable, err) + if varErr := variable.Validate(); varErr != nil { + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrVariable, varErr)) } } for _, constant := range pkg.Constants { - if err := constant.Validate(); err != nil { - return fmt.Errorf(lang.PkgValidateErrConstant, err) + if varErr := constant.Validate(); varErr != nil { + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrConstant, varErr)) } } @@ -71,19 +72,19 @@ func (pkg ZarfPackage) Validate() error { if pkg.Metadata.YOLO { for _, component := range pkg.Components { if len(component.Images) > 0 { - return fmt.Errorf(lang.PkgValidateErrYOLONoOCI) + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrYOLONoOCI)) } if len(component.Repos) > 0 { - return fmt.Errorf(lang.PkgValidateErrYOLONoGit) + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrYOLONoGit)) } if component.Only.Cluster.Architecture != "" { - return fmt.Errorf(lang.PkgValidateErrYOLONoArch) + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrYOLONoArch)) } if len(component.Only.Cluster.Distros) > 0 { - return fmt.Errorf(lang.PkgValidateErrYOLONoDistro) + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrYOLONoDistro)) } } } @@ -91,24 +92,24 @@ func (pkg ZarfPackage) Validate() error { for _, component := range pkg.Components { // ensure component name is unique if _, ok := uniqueComponentNames[component.Name]; ok { - return fmt.Errorf(lang.PkgValidateErrComponentNameNotUnique, component.Name) + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrComponentNameNotUnique, component.Name)) } uniqueComponentNames[component.Name] = true if !IsLowercaseNumberHyphenNoStartHyphen(component.Name) { - return fmt.Errorf(lang.PkgValidateErrComponentName, component.Name) + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrComponentName, component.Name)) } if !slices.Contains(supportedOS, component.Only.LocalOS) { - return fmt.Errorf(lang.PkgValidateErrComponentLocalOS, component.Name, component.Only.LocalOS, supportedOS) + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrComponentLocalOS, component.Name, component.Only.LocalOS, supportedOS)) } if component.IsRequired() { if component.Default { - return fmt.Errorf(lang.PkgValidateErrComponentReqDefault, component.Name) + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrComponentReqDefault, component.Name)) } if component.DeprecatedGroup != "" { - return fmt.Errorf(lang.PkgValidateErrComponentReqGrouped, component.Name) + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrComponentReqGrouped, component.Name)) } } @@ -116,12 +117,12 @@ func (pkg ZarfPackage) Validate() error { for _, chart := range component.Charts { // ensure chart name is unique if _, ok := uniqueChartNames[chart.Name]; ok { - return fmt.Errorf(lang.PkgValidateErrChartNameNotUnique, chart.Name) + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrChartNameNotUnique, chart.Name)) } uniqueChartNames[chart.Name] = true - if err := chart.Validate(); err != nil { - return fmt.Errorf(lang.PkgValidateErrChart, err) + if chartErr := chart.Validate(); chartErr != nil { + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrChart, chartErr)) } } @@ -129,24 +130,24 @@ func (pkg ZarfPackage) Validate() error { for _, manifest := range component.Manifests { // ensure manifest name is unique if _, ok := uniqueManifestNames[manifest.Name]; ok { - return fmt.Errorf(lang.PkgValidateErrManifestNameNotUnique, manifest.Name) + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrManifestNameNotUnique, manifest.Name)) } uniqueManifestNames[manifest.Name] = true - if err := manifest.Validate(); err != nil { - return fmt.Errorf(lang.PkgValidateErrManifest, err) + if manifestErr := manifest.Validate(); manifestErr != nil { + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrManifest, manifestErr)) } } - if err := component.Actions.Validate(); err != nil { - return fmt.Errorf("%q: %w", component.Name, err) + if actionsErr := component.Actions.validate(); actionsErr != nil { + err = errors.Join(err, fmt.Errorf("%q: %w", component.Name, actionsErr)) } // ensure groups don't have multiple defaults or only one component if component.DeprecatedGroup != "" { if component.Default { if _, ok := groupDefault[component.DeprecatedGroup]; ok { - return fmt.Errorf(lang.PkgValidateErrGroupMultipleDefaults, component.DeprecatedGroup, groupDefault[component.DeprecatedGroup], component.Name) + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrGroupMultipleDefaults, component.DeprecatedGroup, groupDefault[component.DeprecatedGroup], component.Name)) } groupDefault[component.DeprecatedGroup] = component.Name } @@ -156,54 +157,54 @@ func (pkg ZarfPackage) Validate() error { for groupKey, componentNames := range groupedComponents { if len(componentNames) == 1 { - return fmt.Errorf(lang.PkgValidateErrGroupOneComponent, groupKey, componentNames[0]) + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrGroupOneComponent, groupKey, componentNames[0])) } } - return nil + return err } -// Validate runs all validation checks on component actions. -func (a ZarfComponentActions) Validate() error { - if err := a.OnCreate.Validate(); err != nil { - return fmt.Errorf(lang.PkgValidateErrAction, err) - } +func (a ZarfComponentActions) validate() error { + var err error + + err = errors.Join(err, a.OnCreate.Validate()) if a.OnCreate.HasSetVariables() { - return fmt.Errorf("cannot contain setVariables outside of onDeploy in actions") + err = errors.Join(err, fmt.Errorf("cannot contain setVariables outside of onDeploy in actions")) } - if err := a.OnDeploy.Validate(); err != nil { - return fmt.Errorf(lang.PkgValidateErrAction, err) - } + err = errors.Join(err, a.OnDeploy.Validate()) if a.OnRemove.HasSetVariables() { - return fmt.Errorf("cannot contain setVariables outside of onDeploy in actions") + err = errors.Join(err, fmt.Errorf("cannot contain setVariables outside of onDeploy in actions")) } - return nil + err = errors.Join(err, a.OnRemove.Validate()) + + return err } -// ValidateImportDefinition validates the component trying to be imported. -func (c ZarfComponent) ValidateImportDefinition() error { +// Validate validates the component trying to be imported. +func (c ZarfComponent) Validate() error { + var err error path := c.Import.Path url := c.Import.URL // ensure path or url is provided if path == "" && url == "" { - return fmt.Errorf(lang.PkgValidateErrImportDefinition, c.Name, "neither a path nor a URL was provided") + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrImportDefinition, c.Name, "neither a path nor a URL was provided")) } // ensure path and url are not both provided if path != "" && url != "" { - return fmt.Errorf(lang.PkgValidateErrImportDefinition, c.Name, "both a path and a URL were provided") + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrImportDefinition, c.Name, "both a path and a URL were provided")) } // validation for path if url == "" && path != "" { // ensure path is not an absolute path if filepath.IsAbs(path) { - return fmt.Errorf(lang.PkgValidateErrImportDefinition, c.Name, "path cannot be an absolute path") + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrImportDefinition, c.Name, "path cannot be an absolute path")) } } @@ -211,11 +212,11 @@ func (c ZarfComponent) ValidateImportDefinition() error { if url != "" && path == "" { ok := helpers.IsOCIURL(url) if !ok { - return fmt.Errorf(lang.PkgValidateErrImportDefinition, c.Name, "URL is not a valid OCI URL") + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrImportDefinition, c.Name, "URL is not a valid OCI URL")) } } - return nil + return err } // HasSetVariables returns true if any of the actions contain setVariables. @@ -234,107 +235,97 @@ func (as ZarfComponentActionSet) HasSetVariables() bool { // Validate runs all validation checks on component action sets. func (as ZarfComponentActionSet) Validate() error { - validate := func(actions []ZarfComponentAction) error { + var err error + validate := func(actions []ZarfComponentAction) { for _, action := range actions { - if err := action.Validate(); err != nil { - return err + if actionErr := action.Validate(); actionErr != nil { + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrAction, actionErr)) } + } - return nil } - if err := validate(as.Before); err != nil { - return err - } - if err := validate(as.After); err != nil { - return err - } - if err := validate(as.OnSuccess); err != nil { - return err - } - return validate(as.OnFailure) + validate(as.Before) + validate(as.After) + validate(as.OnFailure) + validate(as.OnSuccess) + return err } // Validate runs all validation checks on an action. func (action ZarfComponentAction) Validate() error { - // Validate SetVariable + var err error for _, variable := range action.SetVariables { - if err := variable.Validate(); err != nil { - return err - } + err = errors.Join(err, variable.Validate()) } if action.Wait != nil { // Validate only cmd or wait, not both if action.Cmd != "" { - return fmt.Errorf(lang.PkgValidateErrActionCmdWait, action.Cmd) + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrActionCmdWait, action.Cmd)) } // Validate only cluster or network, not both if action.Wait.Cluster != nil && action.Wait.Network != nil { - return fmt.Errorf(lang.PkgValidateErrActionClusterNetwork) + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrActionClusterNetwork)) } // Validate at least one of cluster or network if action.Wait.Cluster == nil && action.Wait.Network == nil { - return fmt.Errorf(lang.PkgValidateErrActionClusterNetwork) + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrActionClusterNetwork)) } } - return nil + return err } // Validate runs all validation checks on a chart. func (chart ZarfChart) Validate() error { - // Don't allow empty names + var err error + if chart.Name == "" { - return fmt.Errorf(lang.PkgValidateErrChartNameMissing, chart.Name) + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrChartNameMissing)) } - // Helm max release name if len(chart.Name) > ZarfMaxChartNameLength { - return fmt.Errorf(lang.PkgValidateErrChartName, chart.Name, ZarfMaxChartNameLength) + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrChartName, chart.Name, ZarfMaxChartNameLength)) } - // Must have a namespace if chart.Namespace == "" { - return fmt.Errorf(lang.PkgValidateErrChartNamespaceMissing, chart.Name) + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrChartNamespaceMissing, chart.Name)) } // Must have a url or localPath (and not both) if chart.URL != "" && chart.LocalPath != "" { - return fmt.Errorf(lang.PkgValidateErrChartURLOrPath, chart.Name) + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrChartURLOrPath, chart.Name)) } - // Must have a url or localPath (and not both) if chart.URL == "" && chart.LocalPath == "" { - return fmt.Errorf(lang.PkgValidateErrChartURLOrPath, chart.Name) + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrChartURLOrPath, chart.Name)) } - // Must have a version if chart.Version == "" { - return fmt.Errorf(lang.PkgValidateErrChartVersion, chart.Name) + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrChartVersion, chart.Name)) } - return nil + return err } // Validate runs all validation checks on a manifest. func (manifest ZarfManifest) Validate() error { - // Don't allow empty names + var err error + if manifest.Name == "" { - return fmt.Errorf(lang.PkgValidateErrManifestNameMissing, manifest.Name) + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrManifestNameMissing)) } - // Helm max release name if len(manifest.Name) > ZarfMaxChartNameLength { - return fmt.Errorf(lang.PkgValidateErrManifestNameLength, manifest.Name, ZarfMaxChartNameLength) + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrManifestNameLength, manifest.Name, ZarfMaxChartNameLength)) } - // Require files in manifest if len(manifest.Files) < 1 && len(manifest.Kustomizations) < 1 { - return fmt.Errorf(lang.PkgValidateErrManifestFileOrKustomize, manifest.Name) + err = errors.Join(err, fmt.Errorf(lang.PkgValidateErrManifestFileOrKustomize, manifest.Name)) } - return nil + return err } diff --git a/src/types/validate_test.go b/src/types/validate_test.go new file mode 100644 index 0000000000..5fcce6821b --- /dev/null +++ b/src/types/validate_test.go @@ -0,0 +1,513 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package types contains all the types used by Zarf. +package types + +import ( + "fmt" + "path/filepath" + "strings" + "testing" + + "github.com/defenseunicorns/pkg/helpers" + "github.com/defenseunicorns/zarf/src/config/lang" + "github.com/defenseunicorns/zarf/src/pkg/variables" + "github.com/stretchr/testify/require" +) + +func TestZarfPackageValidate(t *testing.T) { + t.Parallel() + tests := []struct { + name string + pkg ZarfPackage + expectedErrs []string + }{ + { + name: "valid package", + pkg: ZarfPackage{ + Kind: ZarfPackageConfig, + Metadata: ZarfMetadata{ + Name: "valid-package", + }, + Components: []ZarfComponent{ + { + Name: "component1", + }, + }, + }, + expectedErrs: nil, + }, + { + name: "no components", + pkg: ZarfPackage{ + Kind: ZarfPackageConfig, + Metadata: ZarfMetadata{ + Name: "empty-components", + }, + Components: []ZarfComponent{}, + }, + expectedErrs: []string{"package must have at least 1 component"}, + }, + { + name: "invalid package", + pkg: ZarfPackage{ + Kind: ZarfPackageConfig, + Metadata: ZarfMetadata{ + Name: "-invalid-package", + }, + Components: []ZarfComponent{ + { + Name: "-invalid", + Only: ZarfComponentOnlyTarget{ + LocalOS: "unsupportedOS", + }, + Required: helpers.BoolPtr(true), + Default: true, + Charts: []ZarfChart{ + {Name: "chart1", Namespace: "whatever", URL: "http://whatever", Version: "v1.0.0"}, + {Name: "chart1", Namespace: "whatever", URL: "http://whatever", Version: "v1.0.0"}, + }, + Manifests: []ZarfManifest{ + {Name: "manifest1", Files: []string{"file1"}}, + {Name: "manifest1", Files: []string{"file2"}}, + }, + }, + { + Name: "required-in-group", + Required: helpers.BoolPtr(true), + DeprecatedGroup: "a-group", + }, + { + Name: "multi-default", + Default: true, + DeprecatedGroup: "multi-default", + }, + { + Name: "multi-default-2", + Default: true, + DeprecatedGroup: "multi-default", + }, + { + Name: "duplicate", + }, + { + Name: "duplicate", + }, + }, + Variables: []variables.InteractiveVariable{ + { + Variable: variables.Variable{Name: "not_uppercase"}, + }, + }, + Constants: []variables.Constant{ + { + Name: "not_uppercase", + }, + { + Name: "BAD", + Pattern: "^good_val$", + Value: "bad_val", + }, + }, + }, + expectedErrs: []string{ + fmt.Sprintf(lang.PkgValidateErrPkgName, "-invalid-package"), + fmt.Errorf(lang.PkgValidateErrVariable, fmt.Errorf(lang.PkgValidateMustBeUppercase, "not_uppercase")).Error(), + fmt.Errorf(lang.PkgValidateErrConstant, fmt.Errorf(lang.PkgValidateErrPkgConstantName, "not_uppercase")).Error(), + fmt.Errorf(lang.PkgValidateErrConstant, fmt.Errorf(lang.PkgValidateErrPkgConstantPattern, "BAD", "^good_val$")).Error(), + fmt.Sprintf(lang.PkgValidateErrComponentName, "-invalid"), + fmt.Sprintf(lang.PkgValidateErrComponentLocalOS, "-invalid", "unsupportedOS", supportedOS), + fmt.Sprintf(lang.PkgValidateErrComponentReqDefault, "-invalid"), + fmt.Sprintf(lang.PkgValidateErrChartNameNotUnique, "chart1"), + fmt.Sprintf(lang.PkgValidateErrManifestNameNotUnique, "manifest1"), + fmt.Sprintf(lang.PkgValidateErrComponentReqGrouped, "required-in-group"), + fmt.Sprintf(lang.PkgValidateErrComponentNameNotUnique, "duplicate"), + fmt.Sprintf(lang.PkgValidateErrGroupOneComponent, "a-group", "required-in-group"), + fmt.Sprintf(lang.PkgValidateErrGroupMultipleDefaults, "multi-default", "multi-default", "multi-default-2"), + }, + }, + { + name: "invalid yolo", + pkg: ZarfPackage{ + Kind: ZarfInitConfig, + Metadata: ZarfMetadata{ + Name: "invalid-yolo", + YOLO: true, + }, + Components: []ZarfComponent{ + { + Name: "yolo", + Images: []string{"an-image"}, + Repos: []string{"a-repo"}, + Only: ZarfComponentOnlyTarget{ + Cluster: ZarfComponentOnlyCluster{ + Architecture: "not-empty", + Distros: []string{"not-empty"}, + }, + }, + }, + }, + }, + expectedErrs: []string{ + lang.PkgValidateErrInitNoYOLO, + lang.PkgValidateErrYOLONoOCI, + lang.PkgValidateErrYOLONoGit, + lang.PkgValidateErrYOLONoArch, + lang.PkgValidateErrYOLONoDistro, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + err := tt.pkg.Validate() + if tt.expectedErrs == nil { + require.NoError(t, err) + return + } + errs := strings.Split(err.Error(), "\n") + require.ElementsMatch(t, errs, tt.expectedErrs) + }) + } +} + +func TestValidateManifest(t *testing.T) { + t.Parallel() + longName := strings.Repeat("a", ZarfMaxChartNameLength+1) + tests := []struct { + manifest ZarfManifest + expectedErrs []string + name string + }{ + { + name: "valid", + manifest: ZarfManifest{Name: "valid", Files: []string{"a-file"}}, + expectedErrs: nil, + }, + { + name: "empty name", + manifest: ZarfManifest{Name: "", Files: []string{"a-file"}}, + expectedErrs: []string{lang.PkgValidateErrManifestNameMissing}, + }, + { + name: "long name", + manifest: ZarfManifest{Name: longName, Files: []string{"a-file"}}, + expectedErrs: []string{fmt.Sprintf(lang.PkgValidateErrManifestNameLength, longName, ZarfMaxChartNameLength)}, + }, + { + name: "no files or kustomize", + manifest: ZarfManifest{Name: "nothing-there"}, + expectedErrs: []string{fmt.Sprintf(lang.PkgValidateErrManifestFileOrKustomize, "nothing-there")}, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + err := tt.manifest.Validate() + if tt.expectedErrs == nil { + require.NoError(t, err) + return + } + errs := strings.Split(err.Error(), "\n") + require.ElementsMatch(t, errs, tt.expectedErrs) + }) + } +} + +func TestValidateChart(t *testing.T) { + t.Parallel() + longName := strings.Repeat("a", ZarfMaxChartNameLength+1) + tests := []struct { + chart ZarfChart + expectedErrs []string + name string + }{ + { + name: "valid", + chart: ZarfChart{Name: "chart1", Namespace: "whatever", URL: "http://whatever", Version: "v1.0.0"}, + expectedErrs: nil, + }, + { + name: "empty name", + chart: ZarfChart{Name: "", Namespace: "whatever", URL: "http://whatever", Version: "v1.0.0"}, + expectedErrs: []string{lang.PkgValidateErrChartNameMissing}, + }, + { + name: "long name", + chart: ZarfChart{Name: longName, Namespace: "whatever", URL: "http://whatever", Version: "v1.0.0"}, + expectedErrs: []string{ + fmt.Sprintf(lang.PkgValidateErrChartName, longName, ZarfMaxChartNameLength), + }, + }, + { + name: "no url or local path", + chart: ZarfChart{Name: "invalid"}, + expectedErrs: []string{ + fmt.Sprintf(lang.PkgValidateErrChartNamespaceMissing, "invalid"), + fmt.Sprintf(lang.PkgValidateErrChartURLOrPath, "invalid"), + fmt.Sprintf(lang.PkgValidateErrChartVersion, "invalid"), + }, + }, + { + name: "both url and local path", + chart: ZarfChart{Name: "invalid", Namespace: "whatever", URL: "http://whatever", LocalPath: "wherever", Version: "v1.0.0"}, + expectedErrs: []string{ + fmt.Sprintf(lang.PkgValidateErrChartURLOrPath, "invalid"), + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + err := tt.chart.Validate() + if tt.expectedErrs == nil { + require.NoError(t, err) + return + } + errs := strings.Split(err.Error(), "\n") + require.ElementsMatch(t, tt.expectedErrs, errs) + }) + } +} + +func TestValidateComponentActions(t *testing.T) { + t.Parallel() + tests := []struct { + name string + actions ZarfComponentActions + expectedErrs []string + }{ + { + name: "valid actions", + actions: ZarfComponentActions{ + OnCreate: ZarfComponentActionSet{ + Before: []ZarfComponentAction{ + { + Cmd: "echo 'onCreate before valid'", + }, + }, + }, + OnDeploy: ZarfComponentActionSet{ + Before: []ZarfComponentAction{ + { + Cmd: "echo 'onDeploy before valid'", + }, + }, + }, + }, + expectedErrs: nil, + }, + { + name: "setVariables in onCreate", + actions: ZarfComponentActions{ + OnCreate: ZarfComponentActionSet{ + Before: []ZarfComponentAction{ + { + Cmd: "echo 'invalid setVariable'", + SetVariables: []variables.Variable{{Name: "VAR"}}, + }, + }, + }, + }, + expectedErrs: []string{"cannot contain setVariables outside of onDeploy in actions"}, + }, + { + name: "invalid onCreate action", + actions: ZarfComponentActions{ + OnCreate: ZarfComponentActionSet{ + Before: []ZarfComponentAction{ + { + Cmd: "create", + Wait: &ZarfComponentActionWait{Cluster: &ZarfComponentActionWaitCluster{}}, + }, + }, + }, + OnDeploy: ZarfComponentActionSet{ + After: []ZarfComponentAction{ + { + Cmd: "deploy", + Wait: &ZarfComponentActionWait{Cluster: &ZarfComponentActionWaitCluster{}}, + }, + }, + }, + OnRemove: ZarfComponentActionSet{ + OnSuccess: []ZarfComponentAction{ + { + Cmd: "remove", + Wait: &ZarfComponentActionWait{Cluster: &ZarfComponentActionWaitCluster{}}, + }, + }, + OnFailure: []ZarfComponentAction{ + { + Cmd: "remove2", + Wait: &ZarfComponentActionWait{Cluster: &ZarfComponentActionWaitCluster{}}, + }, + }, + }, + }, + expectedErrs: []string{ + fmt.Errorf(lang.PkgValidateErrAction, fmt.Errorf(lang.PkgValidateErrActionCmdWait, "create")).Error(), + fmt.Errorf(lang.PkgValidateErrAction, fmt.Errorf(lang.PkgValidateErrActionCmdWait, "deploy")).Error(), + fmt.Errorf(lang.PkgValidateErrAction, fmt.Errorf(lang.PkgValidateErrActionCmdWait, "remove")).Error(), + fmt.Errorf(lang.PkgValidateErrAction, fmt.Errorf(lang.PkgValidateErrActionCmdWait, "remove2")).Error(), + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + err := tt.actions.validate() + if tt.expectedErrs == nil { + require.NoError(t, err) + return + } + errs := strings.Split(err.Error(), "\n") + require.ElementsMatch(t, tt.expectedErrs, errs) + }) + } +} + +func TestValidateComponentAction(t *testing.T) { + t.Parallel() + tests := []struct { + name string + action ZarfComponentAction + expectedErrs []string + }{ + { + name: "valid action no conditions", + action: ZarfComponentAction{}, + }, + { + name: "cmd and wait both set, nothing in wait", + action: ZarfComponentAction{ + Cmd: "ls", + Wait: &ZarfComponentActionWait{}, + }, + expectedErrs: []string{ + fmt.Sprintf(lang.PkgValidateErrActionCmdWait, "ls"), + lang.PkgValidateErrActionClusterNetwork, + }, + }, + { + name: "cluster and network both set", + action: ZarfComponentAction{ + Wait: &ZarfComponentActionWait{Cluster: &ZarfComponentActionWaitCluster{}, Network: &ZarfComponentActionWaitNetwork{}}, + }, + expectedErrs: []string{fmt.Sprintf(lang.PkgValidateErrActionClusterNetwork)}, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + err := tt.action.Validate() + if tt.expectedErrs == nil { + require.NoError(t, err) + return + } + errs := strings.Split(err.Error(), "\n") + require.ElementsMatch(t, tt.expectedErrs, errs) + }) + } +} + +func TestValidateZarfComponent(t *testing.T) { + t.Parallel() + absPath, err := filepath.Abs("abs") + require.NoError(t, err) + tests := []struct { + component ZarfComponent + expectedErrs []string + name string + }{ + { + name: "valid path", + component: ZarfComponent{ + Name: "component1", + Import: ZarfComponentImport{ + Path: "relative/path", + }, + }, + expectedErrs: nil, + }, + { + name: "valid URL", + component: ZarfComponent{ + Name: "component2", + Import: ZarfComponentImport{ + URL: "oci://example.com/package:v0.0.1", + }, + }, + expectedErrs: nil, + }, + { + name: "neither path nor URL provided", + component: ZarfComponent{ + Name: "neither", + }, + expectedErrs: []string{ + fmt.Sprintf(lang.PkgValidateErrImportDefinition, "neither", "neither a path nor a URL was provided"), + }, + }, + { + name: "both path and URL provided", + component: ZarfComponent{ + Name: "both", + Import: ZarfComponentImport{ + Path: "relative/path", + URL: "https://example.com", + }, + }, + expectedErrs: []string{ + fmt.Sprintf(lang.PkgValidateErrImportDefinition, "both", "both a path and a URL were provided"), + }, + }, + { + name: "absolute path provided", + component: ZarfComponent{ + Name: "abs-path", + Import: ZarfComponentImport{ + Path: absPath, + }, + }, + expectedErrs: []string{ + fmt.Sprintf(lang.PkgValidateErrImportDefinition, "abs-path", "path cannot be an absolute path"), + }, + }, + { + name: "invalid URL provided", + component: ZarfComponent{ + Name: "bad-url", + Import: ZarfComponentImport{ + URL: "https://example.com", + }, + }, + expectedErrs: []string{ + fmt.Sprintf(lang.PkgValidateErrImportDefinition, "bad-url", "URL is not a valid OCI URL"), + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + err := tt.component.Validate() + if tt.expectedErrs == nil { + require.NoError(t, err) + return + } + errs := strings.Split(err.Error(), "\n") + require.ElementsMatch(t, tt.expectedErrs, errs) + }) + } +} diff --git a/zarf.schema.json b/zarf.schema.json index 27a8a89a4c..0f43c9dbe5 100644 --- a/zarf.schema.json +++ b/zarf.schema.json @@ -420,7 +420,7 @@ "properties": { "name": { "type": "string", - "pattern": "^[a-z0-9\\-]*[a-z0-9]$", + "pattern": "^[a-z0-9][a-z0-9\\-]*$", "description": "The name of the component" }, "description": { @@ -1003,7 +1003,7 @@ "properties": { "name": { "type": "string", - "pattern": "^[a-z0-9\\-]*[a-z0-9]$", + "pattern": "^[a-z0-9][a-z0-9\\-]*$", "description": "Name to identify this Zarf package" }, "description": {