diff --git a/src/internal/packager/validate/validate.go b/src/internal/packager/validate/validate.go deleted file mode 100644 index 48452929a1..0000000000 --- a/src/internal/packager/validate/validate.go +++ /dev/null @@ -1,369 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: 2021-Present The Zarf Authors - -// Package validate provides Zarf package validation functions. -package validate - -import ( - "fmt" - "path/filepath" - "regexp" - "slices" - - "github.com/defenseunicorns/pkg/helpers" - "github.com/defenseunicorns/zarf/src/config" - "github.com/defenseunicorns/zarf/src/config/lang" - "github.com/defenseunicorns/zarf/src/pkg/variables" - "github.com/defenseunicorns/zarf/src/types" -) - -var ( - // IsLowercaseNumberHyphenNoStartHyphen is a regex for lowercase, numbers and hyphens that cannot start with a hyphen. - // https://regex101.com/r/FLdG9G/2 - IsLowercaseNumberHyphenNoStartHyphen = regexp.MustCompile(`^[a-z0-9][a-z0-9\-]*$`).MatchString - // IsUppercaseNumberUnderscore is a regex for uppercase, numbers and underscores. - // https://regex101.com/r/tfsEuZ/1 - IsUppercaseNumberUnderscore = regexp.MustCompile(`^[A-Z0-9_]+$`).MatchString - // Define allowed OS, an empty string means it is allowed on all operating systems - // same as enums on ZarfComponentOnlyTarget - supportedOS = []string{"linux", "darwin", "windows", ""} -) - -// SupportedOS returns the supported operating systems. -// -// The supported operating systems are: linux, darwin, windows. -// -// An empty string signifies no OS restrictions. -func SupportedOS() []string { - return supportedOS -} - -// Run performs config validations. -func Run(pkg types.ZarfPackage) error { - if pkg.Kind == types.ZarfInitConfig && pkg.Metadata.YOLO { - return fmt.Errorf(lang.PkgValidateErrInitNoYOLO) - } - - if err := validatePackageName(pkg.Metadata.Name); err != nil { - return fmt.Errorf(lang.PkgValidateErrName, err) - } - - for _, variable := range pkg.Variables { - if err := validatePackageVariable(variable); err != nil { - return fmt.Errorf(lang.PkgValidateErrVariable, err) - } - } - - for _, constant := range pkg.Constants { - if err := validatePackageConstant(constant); err != nil { - return fmt.Errorf(lang.PkgValidateErrConstant, err) - } - } - - uniqueComponentNames := make(map[string]bool) - groupDefault := make(map[string]string) - groupedComponents := make(map[string][]string) - - for _, component := range pkg.Components { - // ensure component name is unique - if _, ok := uniqueComponentNames[component.Name]; ok { - return fmt.Errorf(lang.PkgValidateErrComponentNameNotUnique, component.Name) - } - uniqueComponentNames[component.Name] = true - - if err := validateComponent(pkg, component); err != nil { - return fmt.Errorf(lang.PkgValidateErrComponent, component.Name, err) - } - - // 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) - } - groupDefault[component.DeprecatedGroup] = component.Name - } - groupedComponents[component.DeprecatedGroup] = append(groupedComponents[component.DeprecatedGroup], component.Name) - } - } - - for groupKey, componentNames := range groupedComponents { - if len(componentNames) == 1 { - return fmt.Errorf(lang.PkgValidateErrGroupOneComponent, groupKey, componentNames[0]) - } - } - - return nil -} - -// ImportDefinition validates the component trying to be imported. -func ImportDefinition(component *types.ZarfComponent) error { - path := component.Import.Path - url := component.Import.URL - - // ensure path or url is provided - if path == "" && url == "" { - return fmt.Errorf(lang.PkgValidateErrImportDefinition, component.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, component.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, component.Name, "path cannot be an absolute path") - } - } - - // validation for url - if url != "" && path == "" { - ok := helpers.IsOCIURL(url) - if !ok { - return fmt.Errorf(lang.PkgValidateErrImportDefinition, component.Name, "URL is not a valid OCI URL") - } - } - - return nil -} - -func oneIfNotEmpty(testString string) int { - if testString == "" { - return 0 - } - - return 1 -} - -func validateComponent(pkg types.ZarfPackage, component types.ZarfComponent) error { - if !IsLowercaseNumberHyphenNoStartHyphen(component.Name) { - return fmt.Errorf(lang.PkgValidateErrComponentName, component.Name) - } - - if !slices.Contains(supportedOS, component.Only.LocalOS) { - return fmt.Errorf(lang.PkgValidateErrComponentLocalOS, component.Name, component.Only.LocalOS, supportedOS) - } - - if component.Required != nil && *component.Required { - if component.Default { - return fmt.Errorf(lang.PkgValidateErrComponentReqDefault, component.Name) - } - if component.DeprecatedGroup != "" { - return fmt.Errorf(lang.PkgValidateErrComponentReqGrouped, component.Name) - } - } - - uniqueChartNames := make(map[string]bool) - for _, chart := range component.Charts { - // ensure chart name is unique - if _, ok := uniqueChartNames[chart.Name]; ok { - return fmt.Errorf(lang.PkgValidateErrChartNameNotUnique, chart.Name) - } - uniqueChartNames[chart.Name] = true - - if err := validateChart(chart); err != nil { - return fmt.Errorf(lang.PkgValidateErrChart, err) - } - } - - uniqueManifestNames := make(map[string]bool) - for _, manifest := range component.Manifests { - // ensure manifest name is unique - if _, ok := uniqueManifestNames[manifest.Name]; ok { - return fmt.Errorf(lang.PkgValidateErrManifestNameNotUnique, manifest.Name) - } - uniqueManifestNames[manifest.Name] = true - - if err := validateManifest(manifest); err != nil { - return fmt.Errorf(lang.PkgValidateErrManifest, err) - } - } - - if pkg.Metadata.YOLO { - if err := validateYOLO(component); err != nil { - return fmt.Errorf(lang.PkgValidateErrComponentYOLO, component.Name, err) - } - } - - if containsVariables, err := validateActionset(component.Actions.OnCreate); err != nil { - return fmt.Errorf(lang.PkgValidateErrAction, err) - } else if containsVariables { - return fmt.Errorf(lang.PkgValidateErrActionVariables, component.Name) - } - - if _, err := validateActionset(component.Actions.OnDeploy); err != nil { - return fmt.Errorf(lang.PkgValidateErrAction, err) - } - - if containsVariables, err := validateActionset(component.Actions.OnRemove); err != nil { - return fmt.Errorf(lang.PkgValidateErrAction, err) - } else if containsVariables { - return fmt.Errorf(lang.PkgValidateErrActionVariables, component.Name) - } - - return nil -} - -func validateActionset(actions types.ZarfComponentActionSet) (bool, error) { - containsVariables := false - - validate := func(actions []types.ZarfComponentAction) error { - for _, action := range actions { - if cv, err := validateAction(action); err != nil { - return err - } else if cv { - containsVariables = true - } - } - - return nil - } - - if err := validate(actions.Before); err != nil { - return containsVariables, err - } - if err := validate(actions.After); err != nil { - return containsVariables, err - } - if err := validate(actions.OnSuccess); err != nil { - return containsVariables, err - } - if err := validate(actions.OnFailure); err != nil { - return containsVariables, err - } - - return containsVariables, nil -} - -func validateAction(action types.ZarfComponentAction) (bool, error) { - containsVariables := false - - // Validate SetVariable - for _, variable := range action.SetVariables { - if !IsUppercaseNumberUnderscore(variable.Name) { - return containsVariables, fmt.Errorf(lang.PkgValidateMustBeUppercase, variable.Name) - } - containsVariables = true - } - - if action.Wait != nil { - // Validate only cmd or wait, not both - if action.Cmd != "" { - return containsVariables, fmt.Errorf(lang.PkgValidateErrActionCmdWait, action.Cmd) - } - - // Validate only cluster or network, not both - if action.Wait.Cluster != nil && action.Wait.Network != nil { - return containsVariables, fmt.Errorf(lang.PkgValidateErrActionClusterNetwork) - } - - // Validate at least one of cluster or network - if action.Wait.Cluster == nil && action.Wait.Network == nil { - return containsVariables, fmt.Errorf(lang.PkgValidateErrActionClusterNetwork) - } - } - - return containsVariables, nil -} - -func validateYOLO(component types.ZarfComponent) error { - if len(component.Images) > 0 { - return fmt.Errorf(lang.PkgValidateErrYOLONoOCI) - } - - if len(component.Repos) > 0 { - return fmt.Errorf(lang.PkgValidateErrYOLONoGit) - } - - if component.Only.Cluster.Architecture != "" { - return fmt.Errorf(lang.PkgValidateErrYOLONoArch) - } - - if len(component.Only.Cluster.Distros) > 0 { - return fmt.Errorf(lang.PkgValidateErrYOLONoDistro) - } - - return nil -} - -func validatePackageName(subject string) error { - if !IsLowercaseNumberHyphenNoStartHyphen(subject) { - return fmt.Errorf(lang.PkgValidateErrPkgName, subject) - } - - return nil -} - -func validatePackageVariable(subject variables.InteractiveVariable) error { - // ensure the variable name is only capitals and underscores - if !IsUppercaseNumberUnderscore(subject.Name) { - return fmt.Errorf(lang.PkgValidateMustBeUppercase, subject.Name) - } - - return nil -} - -func validatePackageConstant(subject variables.Constant) error { - // ensure the constant name is only capitals and underscores - if !IsUppercaseNumberUnderscore(subject.Name) { - return fmt.Errorf(lang.PkgValidateErrPkgConstantName, subject.Name) - } - - if !regexp.MustCompile(subject.Pattern).MatchString(subject.Value) { - return fmt.Errorf(lang.PkgValidateErrPkgConstantPattern, subject.Name, subject.Pattern) - } - - return nil -} - -func validateChart(chart types.ZarfChart) error { - // Don't allow empty names - if chart.Name == "" { - return fmt.Errorf(lang.PkgValidateErrChartNameMissing, chart.Name) - } - - // Helm max release name - if len(chart.Name) > config.ZarfMaxChartNameLength { - return fmt.Errorf(lang.PkgValidateErrChartName, chart.Name, config.ZarfMaxChartNameLength) - } - - // Must have a namespace - if chart.Namespace == "" { - return fmt.Errorf(lang.PkgValidateErrChartNamespaceMissing, chart.Name) - } - - // Must have a url or localPath (and not both) - count := oneIfNotEmpty(chart.URL) + oneIfNotEmpty(chart.LocalPath) - if count != 1 { - return fmt.Errorf(lang.PkgValidateErrChartURLOrPath, chart.Name) - } - - // Must have a version - if chart.Version == "" { - return fmt.Errorf(lang.PkgValidateErrChartVersion, chart.Name) - } - - return nil -} - -func validateManifest(manifest types.ZarfManifest) error { - // Don't allow empty names - if manifest.Name == "" { - return fmt.Errorf(lang.PkgValidateErrManifestNameMissing, manifest.Name) - } - - // Helm max release name - if len(manifest.Name) > config.ZarfMaxChartNameLength { - return fmt.Errorf(lang.PkgValidateErrManifestNameLength, manifest.Name, config.ZarfMaxChartNameLength) - } - - // Require files in manifest - if len(manifest.Files) < 1 && len(manifest.Kustomizations) < 1 { - return fmt.Errorf(lang.PkgValidateErrManifestFileOrKustomize, manifest.Name) - } - - return nil -} diff --git a/src/pkg/packager/composer/list.go b/src/pkg/packager/composer/list.go index dced62b0f1..3642ec562f 100644 --- a/src/pkg/packager/composer/list.go +++ b/src/pkg/packager/composer/list.go @@ -11,7 +11,6 @@ import ( "strings" "github.com/defenseunicorns/pkg/helpers" - "github.com/defenseunicorns/zarf/src/internal/packager/validate" "github.com/defenseunicorns/zarf/src/pkg/layout" "github.com/defenseunicorns/zarf/src/pkg/packager/deprecated" "github.com/defenseunicorns/zarf/src/pkg/utils" @@ -142,7 +141,7 @@ func NewImportChain(head types.ZarfComponent, index int, originalPackageName, ar } // TODO: stuff like this should also happen in linting - if err := validate.ImportDefinition(&node.ZarfComponent); err != nil { + if err := node.ZarfComponent.ValidateImportDefinition(); err != nil { return ic, err } diff --git a/src/pkg/packager/create.go b/src/pkg/packager/create.go index b03664ae13..bc10fe8d73 100755 --- a/src/pkg/packager/create.go +++ b/src/pkg/packager/create.go @@ -10,7 +10,6 @@ import ( "github.com/defenseunicorns/pkg/helpers" "github.com/defenseunicorns/zarf/src/config" - "github.com/defenseunicorns/zarf/src/internal/packager/validate" "github.com/defenseunicorns/zarf/src/pkg/layout" "github.com/defenseunicorns/zarf/src/pkg/message" "github.com/defenseunicorns/zarf/src/pkg/packager/creator" @@ -41,7 +40,7 @@ func (p *Packager) Create() (err error) { } // Perform early package validation. - if err := validate.Run(p.cfg.Pkg); err != nil { + if err := p.cfg.Pkg.Validate(); err != nil { return fmt.Errorf("unable to validate package: %w", err) } diff --git a/src/pkg/packager/dev.go b/src/pkg/packager/dev.go index 454d62f6cc..6f7a685e95 100644 --- a/src/pkg/packager/dev.go +++ b/src/pkg/packager/dev.go @@ -11,7 +11,6 @@ import ( "github.com/defenseunicorns/pkg/helpers" "github.com/defenseunicorns/zarf/src/config" - "github.com/defenseunicorns/zarf/src/internal/packager/validate" "github.com/defenseunicorns/zarf/src/pkg/layout" "github.com/defenseunicorns/zarf/src/pkg/message" "github.com/defenseunicorns/zarf/src/pkg/packager/creator" @@ -53,7 +52,7 @@ func (p *Packager) DevDeploy() error { return err } - if err := validate.Run(p.cfg.Pkg); err != nil { + if err := p.cfg.Pkg.Validate(); err != nil { return fmt.Errorf("unable to validate package: %w", err) } diff --git a/src/pkg/packager/filters/deploy.go b/src/pkg/packager/filters/deploy.go index d98b9c93f2..622f481229 100644 --- a/src/pkg/packager/filters/deploy.go +++ b/src/pkg/packager/filters/deploy.go @@ -75,7 +75,7 @@ func (f *deploymentFilter) Apply(pkg types.ZarfPackage) ([]types.ZarfComponent, selectState, matchedRequest := includedOrExcluded(component.Name, f.requestedComponents) - if !isRequired(component) { + if !component.IsRequired() { if selectState == excluded { // If the component was explicitly excluded, record the match and continue matchedRequests[matchedRequest] = true @@ -161,7 +161,7 @@ func (f *deploymentFilter) Apply(pkg types.ZarfPackage) ([]types.ZarfComponent, } else { component := groupedComponents[groupKey][0] - if isRequired(component) { + if component.IsRequired() { selectedComponents = append(selectedComponents, component) continue } diff --git a/src/pkg/packager/filters/os_test.go b/src/pkg/packager/filters/os_test.go index 9e6b9e7722..7d54beecd3 100644 --- a/src/pkg/packager/filters/os_test.go +++ b/src/pkg/packager/filters/os_test.go @@ -7,7 +7,6 @@ package filters import ( "testing" - "github.com/defenseunicorns/zarf/src/internal/packager/validate" "github.com/defenseunicorns/zarf/src/types" "github.com/stretchr/testify/require" ) @@ -15,7 +14,7 @@ import ( func TestLocalOSFilter(t *testing.T) { pkg := types.ZarfPackage{} - for _, os := range validate.SupportedOS() { + for _, os := range types.SupportedOS() { pkg.Components = append(pkg.Components, types.ZarfComponent{ Only: types.ZarfComponentOnlyTarget{ LocalOS: os, @@ -23,7 +22,7 @@ func TestLocalOSFilter(t *testing.T) { }) } - for _, os := range validate.SupportedOS() { + for _, os := range types.SupportedOS() { filter := ByLocalOS(os) result, err := filter.Apply(pkg) if os == "" { diff --git a/src/pkg/packager/filters/utils.go b/src/pkg/packager/filters/utils.go index 30e1b7cced..3b33f9b594 100644 --- a/src/pkg/packager/filters/utils.go +++ b/src/pkg/packager/filters/utils.go @@ -7,8 +7,6 @@ package filters import ( "path" "strings" - - "github.com/defenseunicorns/zarf/src/types" ) type selectState int @@ -42,14 +40,3 @@ func includedOrExcluded(componentName string, requestedComponentNames []string) // All other cases we don't know if we should include or exclude yet return unknown, "" } - -// isRequired returns if the component is required or not. -func isRequired(c types.ZarfComponent) bool { - requiredExists := c.Required != nil - required := requiredExists && *c.Required - - if requiredExists { - return required - } - return false -} diff --git a/src/pkg/packager/generate.go b/src/pkg/packager/generate.go index 072c1f566b..64bb824285 100644 --- a/src/pkg/packager/generate.go +++ b/src/pkg/packager/generate.go @@ -12,7 +12,6 @@ import ( "github.com/defenseunicorns/pkg/helpers" "github.com/defenseunicorns/zarf/src/config" - "github.com/defenseunicorns/zarf/src/internal/packager/validate" "github.com/defenseunicorns/zarf/src/pkg/layout" "github.com/defenseunicorns/zarf/src/pkg/message" "github.com/defenseunicorns/zarf/src/types" @@ -73,7 +72,7 @@ func (p *Packager) Generate() (err error) { p.cfg.Pkg.Components[i].Images = images[name] } - if err := validate.Run(p.cfg.Pkg); err != nil { + if err := p.cfg.Pkg.Validate(); err != nil { return err } diff --git a/src/pkg/packager/sources/cluster.go b/src/pkg/packager/sources/cluster.go index 1d506c6937..9fb74d6e81 100644 --- a/src/pkg/packager/sources/cluster.go +++ b/src/pkg/packager/sources/cluster.go @@ -8,7 +8,6 @@ import ( "fmt" "github.com/defenseunicorns/pkg/helpers" - "github.com/defenseunicorns/zarf/src/internal/packager/validate" "github.com/defenseunicorns/zarf/src/pkg/cluster" "github.com/defenseunicorns/zarf/src/pkg/layout" "github.com/defenseunicorns/zarf/src/pkg/packager/filters" @@ -23,7 +22,7 @@ var ( // NewClusterSource creates a new cluster source. func NewClusterSource(pkgOpts *types.ZarfPackageOptions) (PackageSource, error) { - if !validate.IsLowercaseNumberHyphenNoStartHyphen(pkgOpts.PackageSource) { + if !types.IsLowercaseNumberHyphenNoStartHyphen(pkgOpts.PackageSource) { return nil, fmt.Errorf("invalid package name %q", pkgOpts.PackageSource) } cluster, err := cluster.NewClusterWithWait(cluster.DefaultTimeout) diff --git a/src/pkg/variables/types.go b/src/pkg/variables/types.go index 5b56fe3eeb..f6f51a6917 100644 --- a/src/pkg/variables/types.go +++ b/src/pkg/variables/types.go @@ -4,6 +4,13 @@ // Package variables contains functions for interacting with variables package variables +import ( + "fmt" + "regexp" + + "github.com/defenseunicorns/zarf/src/config/lang" +) + // VariableType represents a type of a Zarf package variable type VariableType string @@ -14,6 +21,12 @@ const ( FileVariableType VariableType = "file" ) +var ( + // IsUppercaseNumberUnderscore is a regex for uppercase, numbers and underscores. + // https://regex101.com/r/tfsEuZ/1 + IsUppercaseNumberUnderscore = regexp.MustCompile(`^[A-Z0-9_]+$`).MatchString +) + // Variable represents a variable that has a value set programmatically type Variable struct { Name string `json:"name" jsonschema:"description=The name to be used for the variable,pattern=^[A-Z0-9_]+$"` @@ -46,3 +59,25 @@ type SetVariable struct { Variable `json:",inline"` Value string `json:"value" jsonschema:"description=The value the variable is currently set with"` } + +// Validate runs all validation checks on a package variable. +func (v Variable) Validate() error { + if !IsUppercaseNumberUnderscore(v.Name) { + return fmt.Errorf(lang.PkgValidateMustBeUppercase, v.Name) + } + return nil +} + +// Validate runs all validation checks on a package constant. +func (c Constant) Validate() error { + // ensure the constant name is only capitals and underscores + if !IsUppercaseNumberUnderscore(c.Name) { + return fmt.Errorf(lang.PkgValidateErrPkgConstantName, c.Name) + } + + if !regexp.MustCompile(c.Pattern).MatchString(c.Value) { + return fmt.Errorf(lang.PkgValidateErrPkgConstantPattern, c.Name, c.Pattern) + } + + return nil +} diff --git a/src/types/component.go b/src/types/component.go index caee8ee680..3c4ffa21c4 100644 --- a/src/types/component.go +++ b/src/types/component.go @@ -81,6 +81,15 @@ func (c ZarfComponent) RequiresCluster() bool { return false } +// IsRequired returns if the component is required or not. +func (c ZarfComponent) IsRequired() bool { + if c.Required != nil { + return *c.Required + } + + return false +} + // ZarfComponentOnlyTarget filters a component to only show it for a given local OS and cluster. type ZarfComponentOnlyTarget struct { LocalOS string `json:"localOS,omitempty" jsonschema:"description=Only deploy component to specified OS,enum=linux,enum=darwin,enum=windows"` diff --git a/src/types/validate.go b/src/types/validate.go new file mode 100644 index 0000000000..ae6236227f --- /dev/null +++ b/src/types/validate.go @@ -0,0 +1,335 @@ +// 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" + "regexp" + "slices" + + "github.com/defenseunicorns/pkg/helpers" + "github.com/defenseunicorns/zarf/src/config/lang" +) + +const ( + // ZarfMaxChartNameLength limits helm chart name size to account for K8s/helm limits and zarf prefix + ZarfMaxChartNameLength = 40 +) + +var ( + // IsLowercaseNumberHyphenNoStartHyphen is a regex for lowercase, numbers and hyphens that cannot start with a hyphen. + // https://regex101.com/r/FLdG9G/2 + IsLowercaseNumberHyphenNoStartHyphen = regexp.MustCompile(`^[a-z0-9][a-z0-9\-]*$`).MatchString + // Define allowed OS, an empty string means it is allowed on all operating systems + // same as enums on ZarfComponentOnlyTarget + supportedOS = []string{"linux", "darwin", "windows", ""} +) + +// SupportedOS returns the supported operating systems. +// +// The supported operating systems are: linux, darwin, windows. +// +// An empty string signifies no OS restrictions. +func SupportedOS() []string { + return supportedOS +} + +// Validate runs all validation checks on the package. +func (pkg ZarfPackage) Validate() error { + if pkg.Kind == ZarfInitConfig && pkg.Metadata.YOLO { + return fmt.Errorf(lang.PkgValidateErrInitNoYOLO) + } + + if !IsLowercaseNumberHyphenNoStartHyphen(pkg.Metadata.Name) { + return fmt.Errorf(lang.PkgValidateErrPkgName, pkg.Metadata.Name) + } + + for _, variable := range pkg.Variables { + if err := variable.Validate(); err != nil { + return fmt.Errorf(lang.PkgValidateErrVariable, err) + } + } + + for _, constant := range pkg.Constants { + if err := constant.Validate(); err != nil { + return fmt.Errorf(lang.PkgValidateErrConstant, err) + } + } + + uniqueComponentNames := make(map[string]bool) + groupDefault := make(map[string]string) + groupedComponents := make(map[string][]string) + + if pkg.Metadata.YOLO { + for _, component := range pkg.Components { + if len(component.Images) > 0 { + return fmt.Errorf(lang.PkgValidateErrYOLONoOCI) + } + + if len(component.Repos) > 0 { + return fmt.Errorf(lang.PkgValidateErrYOLONoGit) + } + + if component.Only.Cluster.Architecture != "" { + return fmt.Errorf(lang.PkgValidateErrYOLONoArch) + } + + if len(component.Only.Cluster.Distros) > 0 { + return fmt.Errorf(lang.PkgValidateErrYOLONoDistro) + } + } + } + + for _, component := range pkg.Components { + // ensure component name is unique + if _, ok := uniqueComponentNames[component.Name]; ok { + return fmt.Errorf(lang.PkgValidateErrComponentNameNotUnique, component.Name) + } + uniqueComponentNames[component.Name] = true + + if !IsLowercaseNumberHyphenNoStartHyphen(component.Name) { + return fmt.Errorf(lang.PkgValidateErrComponentName, component.Name) + } + + if !slices.Contains(supportedOS, component.Only.LocalOS) { + return fmt.Errorf(lang.PkgValidateErrComponentLocalOS, component.Name, component.Only.LocalOS, supportedOS) + } + + if component.IsRequired() { + if component.Default { + return fmt.Errorf(lang.PkgValidateErrComponentReqDefault, component.Name) + } + if component.DeprecatedGroup != "" { + return fmt.Errorf(lang.PkgValidateErrComponentReqGrouped, component.Name) + } + } + + uniqueChartNames := make(map[string]bool) + for _, chart := range component.Charts { + // ensure chart name is unique + if _, ok := uniqueChartNames[chart.Name]; ok { + return fmt.Errorf(lang.PkgValidateErrChartNameNotUnique, chart.Name) + } + uniqueChartNames[chart.Name] = true + + if err := chart.Validate(); err != nil { + return fmt.Errorf(lang.PkgValidateErrChart, err) + } + } + + uniqueManifestNames := make(map[string]bool) + for _, manifest := range component.Manifests { + // ensure manifest name is unique + if _, ok := uniqueManifestNames[manifest.Name]; ok { + return fmt.Errorf(lang.PkgValidateErrManifestNameNotUnique, manifest.Name) + } + uniqueManifestNames[manifest.Name] = true + + if err := manifest.Validate(); err != nil { + return fmt.Errorf(lang.PkgValidateErrManifest, err) + } + } + + if err := component.Actions.Validate(); err != nil { + return fmt.Errorf("%q: %w", component.Name, err) + } + + // 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) + } + groupDefault[component.DeprecatedGroup] = component.Name + } + groupedComponents[component.DeprecatedGroup] = append(groupedComponents[component.DeprecatedGroup], component.Name) + } + } + + for groupKey, componentNames := range groupedComponents { + if len(componentNames) == 1 { + return fmt.Errorf(lang.PkgValidateErrGroupOneComponent, groupKey, componentNames[0]) + } + } + + return nil +} + +// 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) + } + + if a.OnCreate.HasSetVariables() { + return fmt.Errorf("cannot contain setVariables outside of onDeploy in actions") + } + + if err := a.OnDeploy.Validate(); err != nil { + return fmt.Errorf(lang.PkgValidateErrAction, err) + } + + if a.OnRemove.HasSetVariables() { + return fmt.Errorf("cannot contain setVariables outside of onDeploy in actions") + } + + return nil +} + +// ValidateImportDefinition validates the component trying to be imported. +func (c ZarfComponent) ValidateImportDefinition() 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") + } + + // 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") + } + + // 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") + } + } + + // validation for url + if url != "" && path == "" { + ok := helpers.IsOCIURL(url) + if !ok { + return fmt.Errorf(lang.PkgValidateErrImportDefinition, c.Name, "URL is not a valid OCI URL") + } + } + + return nil +} + +// HasSetVariables returns true if any of the actions contain setVariables. +func (as ZarfComponentActionSet) HasSetVariables() bool { + check := func(actions []ZarfComponentAction) bool { + for _, action := range actions { + if len(action.SetVariables) > 0 { + return true + } + } + return false + } + + return check(as.Before) || check(as.After) || check(as.OnSuccess) || check(as.OnFailure) +} + +// Validate runs all validation checks on component action sets. +func (as ZarfComponentActionSet) Validate() error { + validate := func(actions []ZarfComponentAction) error { + for _, action := range actions { + if err := action.Validate(); err != nil { + return err + } + } + 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 runs all validation checks on an action. +func (action ZarfComponentAction) Validate() error { + // Validate SetVariable + for _, variable := range action.SetVariables { + if err := variable.Validate(); err != nil { + return err + } + } + + if action.Wait != nil { + // Validate only cmd or wait, not both + if action.Cmd != "" { + return 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) + } + + // Validate at least one of cluster or network + if action.Wait.Cluster == nil && action.Wait.Network == nil { + return fmt.Errorf(lang.PkgValidateErrActionClusterNetwork) + } + } + + return nil +} + +// Validate runs all validation checks on a chart. +func (chart ZarfChart) Validate() error { + // Don't allow empty names + if chart.Name == "" { + return fmt.Errorf(lang.PkgValidateErrChartNameMissing, chart.Name) + } + + // Helm max release name + if len(chart.Name) > ZarfMaxChartNameLength { + return fmt.Errorf(lang.PkgValidateErrChartName, chart.Name, ZarfMaxChartNameLength) + } + + // Must have a namespace + if chart.Namespace == "" { + return 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) + } + + // Must have a url or localPath (and not both) + if chart.URL == "" && chart.LocalPath == "" { + return fmt.Errorf(lang.PkgValidateErrChartURLOrPath, chart.Name) + } + + // Must have a version + if chart.Version == "" { + return fmt.Errorf(lang.PkgValidateErrChartVersion, chart.Name) + } + + return nil +} + +// Validate runs all validation checks on a manifest. +func (manifest ZarfManifest) Validate() error { + // Don't allow empty names + if manifest.Name == "" { + return fmt.Errorf(lang.PkgValidateErrManifestNameMissing, manifest.Name) + } + + // Helm max release name + if len(manifest.Name) > ZarfMaxChartNameLength { + return 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) + } + + return nil +}