github.com/Racer159/jackal@v0.32.7-0.20240401174413-0bd2339e4f2e/src/internal/packager/validate/validate.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // SPDX-FileCopyrightText: 2021-Present The Jackal Authors
     3  
     4  // Package validate provides Jackal package validation functions.
     5  package validate
     6  
     7  import (
     8  	"fmt"
     9  	"path/filepath"
    10  	"regexp"
    11  	"slices"
    12  
    13  	"github.com/Racer159/jackal/src/config"
    14  	"github.com/Racer159/jackal/src/config/lang"
    15  	"github.com/Racer159/jackal/src/types"
    16  	"github.com/defenseunicorns/pkg/helpers"
    17  )
    18  
    19  var (
    20  	// IsLowercaseNumberHyphenNoStartHyphen is a regex for lowercase, numbers and hyphens that cannot start with a hyphen.
    21  	// https://regex101.com/r/FLdG9G/2
    22  	IsLowercaseNumberHyphenNoStartHyphen = regexp.MustCompile(`^[a-z0-9][a-z0-9\-]*$`).MatchString
    23  	// IsUppercaseNumberUnderscore is a regex for uppercase, numbers and underscores.
    24  	// https://regex101.com/r/tfsEuZ/1
    25  	IsUppercaseNumberUnderscore = regexp.MustCompile(`^[A-Z0-9_]+$`).MatchString
    26  	// Define allowed OS, an empty string means it is allowed on all operating systems
    27  	// same as enums on JackalComponentOnlyTarget
    28  	supportedOS = []string{"linux", "darwin", "windows", ""}
    29  )
    30  
    31  // SupportedOS returns the supported operating systems.
    32  //
    33  // The supported operating systems are: linux, darwin, windows.
    34  //
    35  // An empty string signifies no OS restrictions.
    36  func SupportedOS() []string {
    37  	return supportedOS
    38  }
    39  
    40  // Run performs config validations.
    41  func Run(pkg types.JackalPackage) error {
    42  	if pkg.Kind == types.JackalInitConfig && pkg.Metadata.YOLO {
    43  		return fmt.Errorf(lang.PkgValidateErrInitNoYOLO)
    44  	}
    45  
    46  	if err := validatePackageName(pkg.Metadata.Name); err != nil {
    47  		return fmt.Errorf(lang.PkgValidateErrName, err)
    48  	}
    49  
    50  	for _, variable := range pkg.Variables {
    51  		if err := validatePackageVariable(variable); err != nil {
    52  			return fmt.Errorf(lang.PkgValidateErrVariable, err)
    53  		}
    54  	}
    55  
    56  	for _, constant := range pkg.Constants {
    57  		if err := validatePackageConstant(constant); err != nil {
    58  			return fmt.Errorf(lang.PkgValidateErrConstant, err)
    59  		}
    60  	}
    61  
    62  	uniqueComponentNames := make(map[string]bool)
    63  	groupDefault := make(map[string]string)
    64  	groupedComponents := make(map[string][]string)
    65  
    66  	for _, component := range pkg.Components {
    67  		// ensure component name is unique
    68  		if _, ok := uniqueComponentNames[component.Name]; ok {
    69  			return fmt.Errorf(lang.PkgValidateErrComponentNameNotUnique, component.Name)
    70  		}
    71  		uniqueComponentNames[component.Name] = true
    72  
    73  		if err := validateComponent(pkg, component); err != nil {
    74  			return fmt.Errorf(lang.PkgValidateErrComponent, component.Name, err)
    75  		}
    76  
    77  		// ensure groups don't have multiple defaults or only one component
    78  		if component.DeprecatedGroup != "" {
    79  			if component.Default {
    80  				if _, ok := groupDefault[component.DeprecatedGroup]; ok {
    81  					return fmt.Errorf(lang.PkgValidateErrGroupMultipleDefaults, component.DeprecatedGroup, groupDefault[component.DeprecatedGroup], component.Name)
    82  				}
    83  				groupDefault[component.DeprecatedGroup] = component.Name
    84  			}
    85  			groupedComponents[component.DeprecatedGroup] = append(groupedComponents[component.DeprecatedGroup], component.Name)
    86  		}
    87  	}
    88  
    89  	for groupKey, componentNames := range groupedComponents {
    90  		if len(componentNames) == 1 {
    91  			return fmt.Errorf(lang.PkgValidateErrGroupOneComponent, groupKey, componentNames[0])
    92  		}
    93  	}
    94  
    95  	return nil
    96  }
    97  
    98  // ImportDefinition validates the component trying to be imported.
    99  func ImportDefinition(component *types.JackalComponent) error {
   100  	path := component.Import.Path
   101  	url := component.Import.URL
   102  
   103  	// ensure path or url is provided
   104  	if path == "" && url == "" {
   105  		return fmt.Errorf(lang.PkgValidateErrImportDefinition, component.Name, "neither a path nor a URL was provided")
   106  	}
   107  
   108  	// ensure path and url are not both provided
   109  	if path != "" && url != "" {
   110  		return fmt.Errorf(lang.PkgValidateErrImportDefinition, component.Name, "both a path and a URL were provided")
   111  	}
   112  
   113  	// validation for path
   114  	if url == "" && path != "" {
   115  		// ensure path is not an absolute path
   116  		if filepath.IsAbs(path) {
   117  			return fmt.Errorf(lang.PkgValidateErrImportDefinition, component.Name, "path cannot be an absolute path")
   118  		}
   119  	}
   120  
   121  	// validation for url
   122  	if url != "" && path == "" {
   123  		ok := helpers.IsOCIURL(url)
   124  		if !ok {
   125  			return fmt.Errorf(lang.PkgValidateErrImportDefinition, component.Name, "URL is not a valid OCI URL")
   126  		}
   127  	}
   128  
   129  	return nil
   130  }
   131  
   132  func oneIfNotEmpty(testString string) int {
   133  	if testString == "" {
   134  		return 0
   135  	}
   136  
   137  	return 1
   138  }
   139  
   140  func validateComponent(pkg types.JackalPackage, component types.JackalComponent) error {
   141  	if !IsLowercaseNumberHyphenNoStartHyphen(component.Name) {
   142  		return fmt.Errorf(lang.PkgValidateErrComponentName, component.Name)
   143  	}
   144  
   145  	if !slices.Contains(supportedOS, component.Only.LocalOS) {
   146  		return fmt.Errorf(lang.PkgValidateErrComponentLocalOS, component.Name, component.Only.LocalOS, supportedOS)
   147  	}
   148  
   149  	if component.Required != nil && *component.Required {
   150  		if component.Default {
   151  			return fmt.Errorf(lang.PkgValidateErrComponentReqDefault, component.Name)
   152  		}
   153  		if component.DeprecatedGroup != "" {
   154  			return fmt.Errorf(lang.PkgValidateErrComponentReqGrouped, component.Name)
   155  		}
   156  	}
   157  
   158  	uniqueChartNames := make(map[string]bool)
   159  	for _, chart := range component.Charts {
   160  		// ensure chart name is unique
   161  		if _, ok := uniqueChartNames[chart.Name]; ok {
   162  			return fmt.Errorf(lang.PkgValidateErrChartNameNotUnique, chart.Name)
   163  		}
   164  		uniqueChartNames[chart.Name] = true
   165  
   166  		if err := validateChart(chart); err != nil {
   167  			return fmt.Errorf(lang.PkgValidateErrChart, err)
   168  		}
   169  	}
   170  
   171  	uniqueManifestNames := make(map[string]bool)
   172  	for _, manifest := range component.Manifests {
   173  		// ensure manifest name is unique
   174  		if _, ok := uniqueManifestNames[manifest.Name]; ok {
   175  			return fmt.Errorf(lang.PkgValidateErrManifestNameNotUnique, manifest.Name)
   176  		}
   177  		uniqueManifestNames[manifest.Name] = true
   178  
   179  		if err := validateManifest(manifest); err != nil {
   180  			return fmt.Errorf(lang.PkgValidateErrManifest, err)
   181  		}
   182  	}
   183  
   184  	if pkg.Metadata.YOLO {
   185  		if err := validateYOLO(component); err != nil {
   186  			return fmt.Errorf(lang.PkgValidateErrComponentYOLO, component.Name, err)
   187  		}
   188  	}
   189  
   190  	if containsVariables, err := validateActionset(component.Actions.OnCreate); err != nil {
   191  		return fmt.Errorf(lang.PkgValidateErrAction, err)
   192  	} else if containsVariables {
   193  		return fmt.Errorf(lang.PkgValidateErrActionVariables, component.Name)
   194  	}
   195  
   196  	if _, err := validateActionset(component.Actions.OnDeploy); err != nil {
   197  		return fmt.Errorf(lang.PkgValidateErrAction, err)
   198  	}
   199  
   200  	if containsVariables, err := validateActionset(component.Actions.OnRemove); err != nil {
   201  		return fmt.Errorf(lang.PkgValidateErrAction, err)
   202  	} else if containsVariables {
   203  		return fmt.Errorf(lang.PkgValidateErrActionVariables, component.Name)
   204  	}
   205  
   206  	return nil
   207  }
   208  
   209  func validateActionset(actions types.JackalComponentActionSet) (bool, error) {
   210  	containsVariables := false
   211  
   212  	validate := func(actions []types.JackalComponentAction) error {
   213  		for _, action := range actions {
   214  			if cv, err := validateAction(action); err != nil {
   215  				return err
   216  			} else if cv {
   217  				containsVariables = true
   218  			}
   219  		}
   220  
   221  		return nil
   222  	}
   223  
   224  	if err := validate(actions.Before); err != nil {
   225  		return containsVariables, err
   226  	}
   227  	if err := validate(actions.After); err != nil {
   228  		return containsVariables, err
   229  	}
   230  	if err := validate(actions.OnSuccess); err != nil {
   231  		return containsVariables, err
   232  	}
   233  	if err := validate(actions.OnFailure); err != nil {
   234  		return containsVariables, err
   235  	}
   236  
   237  	return containsVariables, nil
   238  }
   239  
   240  func validateAction(action types.JackalComponentAction) (bool, error) {
   241  	containsVariables := false
   242  
   243  	// Validate SetVariable
   244  	for _, variable := range action.SetVariables {
   245  		if !IsUppercaseNumberUnderscore(variable.Name) {
   246  			return containsVariables, fmt.Errorf(lang.PkgValidateMustBeUppercase, variable.Name)
   247  		}
   248  		containsVariables = true
   249  	}
   250  
   251  	if action.Wait != nil {
   252  		// Validate only cmd or wait, not both
   253  		if action.Cmd != "" {
   254  			return containsVariables, fmt.Errorf(lang.PkgValidateErrActionCmdWait, action.Cmd)
   255  		}
   256  
   257  		// Validate only cluster or network, not both
   258  		if action.Wait.Cluster != nil && action.Wait.Network != nil {
   259  			return containsVariables, fmt.Errorf(lang.PkgValidateErrActionClusterNetwork)
   260  		}
   261  
   262  		// Validate at least one of cluster or network
   263  		if action.Wait.Cluster == nil && action.Wait.Network == nil {
   264  			return containsVariables, fmt.Errorf(lang.PkgValidateErrActionClusterNetwork)
   265  		}
   266  	}
   267  
   268  	return containsVariables, nil
   269  }
   270  
   271  func validateYOLO(component types.JackalComponent) error {
   272  	if len(component.Images) > 0 {
   273  		return fmt.Errorf(lang.PkgValidateErrYOLONoOCI)
   274  	}
   275  
   276  	if len(component.Repos) > 0 {
   277  		return fmt.Errorf(lang.PkgValidateErrYOLONoGit)
   278  	}
   279  
   280  	if component.Only.Cluster.Architecture != "" {
   281  		return fmt.Errorf(lang.PkgValidateErrYOLONoArch)
   282  	}
   283  
   284  	if len(component.Only.Cluster.Distros) > 0 {
   285  		return fmt.Errorf(lang.PkgValidateErrYOLONoDistro)
   286  	}
   287  
   288  	return nil
   289  }
   290  
   291  func validatePackageName(subject string) error {
   292  	if !IsLowercaseNumberHyphenNoStartHyphen(subject) {
   293  		return fmt.Errorf(lang.PkgValidateErrPkgName, subject)
   294  	}
   295  
   296  	return nil
   297  }
   298  
   299  func validatePackageVariable(subject types.JackalPackageVariable) error {
   300  	// ensure the variable name is only capitals and underscores
   301  	if !IsUppercaseNumberUnderscore(subject.Name) {
   302  		return fmt.Errorf(lang.PkgValidateMustBeUppercase, subject.Name)
   303  	}
   304  
   305  	return nil
   306  }
   307  
   308  func validatePackageConstant(subject types.JackalPackageConstant) error {
   309  	// ensure the constant name is only capitals and underscores
   310  	if !IsUppercaseNumberUnderscore(subject.Name) {
   311  		return fmt.Errorf(lang.PkgValidateErrPkgConstantName, subject.Name)
   312  	}
   313  
   314  	if !regexp.MustCompile(subject.Pattern).MatchString(subject.Value) {
   315  		return fmt.Errorf(lang.PkgValidateErrPkgConstantPattern, subject.Name, subject.Pattern)
   316  	}
   317  
   318  	return nil
   319  }
   320  
   321  func validateChart(chart types.JackalChart) error {
   322  	// Don't allow empty names
   323  	if chart.Name == "" {
   324  		return fmt.Errorf(lang.PkgValidateErrChartNameMissing, chart.Name)
   325  	}
   326  
   327  	// Helm max release name
   328  	if len(chart.Name) > config.JackalMaxChartNameLength {
   329  		return fmt.Errorf(lang.PkgValidateErrChartName, chart.Name, config.JackalMaxChartNameLength)
   330  	}
   331  
   332  	// Must have a namespace
   333  	if chart.Namespace == "" {
   334  		return fmt.Errorf(lang.PkgValidateErrChartNamespaceMissing, chart.Name)
   335  	}
   336  
   337  	// Must have a url or localPath (and not both)
   338  	count := oneIfNotEmpty(chart.URL) + oneIfNotEmpty(chart.LocalPath)
   339  	if count != 1 {
   340  		return fmt.Errorf(lang.PkgValidateErrChartURLOrPath, chart.Name)
   341  	}
   342  
   343  	// Must have a version
   344  	if chart.Version == "" {
   345  		return fmt.Errorf(lang.PkgValidateErrChartVersion, chart.Name)
   346  	}
   347  
   348  	return nil
   349  }
   350  
   351  func validateManifest(manifest types.JackalManifest) error {
   352  	// Don't allow empty names
   353  	if manifest.Name == "" {
   354  		return fmt.Errorf(lang.PkgValidateErrManifestNameMissing, manifest.Name)
   355  	}
   356  
   357  	// Helm max release name
   358  	if len(manifest.Name) > config.JackalMaxChartNameLength {
   359  		return fmt.Errorf(lang.PkgValidateErrManifestNameLength, manifest.Name, config.JackalMaxChartNameLength)
   360  	}
   361  
   362  	// Require files in manifest
   363  	if len(manifest.Files) < 1 && len(manifest.Kustomizations) < 1 {
   364  		return fmt.Errorf(lang.PkgValidateErrManifestFileOrKustomize, manifest.Name)
   365  	}
   366  
   367  	return nil
   368  }