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 }