github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/pkg/config/evaluation.go (about) 1 package config 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "path" 8 "strings" 9 10 "github.com/SAP/jenkins-library/pkg/orchestrator" 11 "github.com/SAP/jenkins-library/pkg/piperutils" 12 13 "github.com/pkg/errors" 14 ) 15 16 const ( 17 configCondition = "config" 18 configKeysCondition = "configKeys" 19 filePatternFromConfigCondition = "filePatternFromConfig" 20 filePatternCondition = "filePattern" 21 npmScriptsCondition = "npmScripts" 22 ) 23 24 // evaluateConditionsV1 validates stage conditions and updates runSteps in runConfig according to V1 schema. 25 // Priority of step activation/deactivation is follow: 26 // - stepNotActiveCondition (highest, if any) 27 // - explicit activation/deactivation (medium, if any) 28 // - stepActiveConditions (lowest, step is active by default if no conditions are configured) 29 func (r *RunConfigV1) evaluateConditionsV1(config *Config, utils piperutils.FileUtils, envRootPath string) error { 30 if r.RunSteps == nil { 31 r.RunSteps = make(map[string]map[string]bool, len(r.PipelineConfig.Spec.Stages)) 32 } 33 if r.RunStages == nil { 34 r.RunStages = make(map[string]bool, len(r.PipelineConfig.Spec.Stages)) 35 } 36 37 currentOrchestrator := orchestrator.DetectOrchestrator().String() 38 for _, stage := range r.PipelineConfig.Spec.Stages { 39 // Currently, the displayName is being used, but it may be necessary 40 // to also consider using the technical name. 41 stageName := stage.DisplayName 42 43 // Check #1: Apply explicit activation/deactivation from config file (if any) 44 // and then evaluate stepActive conditions 45 runStep := make(map[string]bool, len(stage.Steps)) 46 stepConfigCache := make(map[string]StepConfig, len(stage.Steps)) 47 for _, step := range stage.Steps { 48 // Consider only orchestrator-specific steps if the orchestrator limitation is set. 49 if len(step.Orchestrators) > 0 && !piperutils.ContainsString(step.Orchestrators, currentOrchestrator) { 50 continue 51 } 52 53 stepConfig, err := r.getStepConfig(config, stageName, step.Name, nil, nil, nil, nil) 54 if err != nil { 55 return err 56 } 57 stepConfigCache[step.Name] = stepConfig 58 59 // Respect explicit activation/deactivation if available. 60 // Note that this has higher priority than step conditions 61 if active, ok := stepConfig.Config[step.Name].(bool); ok { 62 runStep[step.Name] = active 63 continue 64 } 65 66 // If no condition is available, the step will be active by default. 67 stepActive := true 68 for _, condition := range step.Conditions { 69 stepActive, err = condition.evaluateV1(stepConfig, utils, step.Name, envRootPath, runStep) 70 if err != nil { 71 return fmt.Errorf("failed to evaluate step conditions: %w", err) 72 } 73 if stepActive { 74 // The first condition that matches will be considered to activate the step. 75 break 76 } 77 } 78 79 runStep[step.Name] = stepActive 80 } 81 82 // Check #2: Evaluate stepNotActive conditions (if any) and deactivate the step if the condition is met. 83 // 84 // TODO: PART 1 : if explicit activation/de-activation is available should notActiveConditions be checked ? 85 // Fortify has no anchor, so if we explicitly set it to true then it may run even during commit pipelines, if we implement TODO PART 1?? 86 for _, step := range stage.Steps { 87 stepConfig, found := stepConfigCache[step.Name] 88 if !found { 89 // If no stepConfig exists here, it means that this step was skipped in previous checks. 90 continue 91 } 92 93 for _, condition := range step.NotActiveConditions { 94 stepNotActive, err := condition.evaluateV1(stepConfig, utils, step.Name, envRootPath, runStep) 95 if err != nil { 96 return fmt.Errorf("failed to evaluate not active step conditions: %w", err) 97 } 98 99 // Deactivate the step if the notActive condition is met. 100 if stepNotActive { 101 runStep[step.Name] = false 102 break 103 } 104 } 105 } 106 107 r.RunSteps[stageName] = runStep 108 109 stageActive := false 110 for _, anyStepIsActive := range r.RunSteps[stageName] { 111 if anyStepIsActive { 112 stageActive = true 113 } 114 } 115 r.RunStages[stageName] = stageActive 116 } 117 118 return nil 119 } 120 121 func (s *StepCondition) evaluateV1( 122 config StepConfig, 123 utils piperutils.FileUtils, 124 stepName string, 125 envRootPath string, 126 runSteps map[string]bool, 127 ) (bool, error) { 128 129 // only the first condition will be evaluated. 130 // if multiple conditions should be checked they need to provided via the Conditions list 131 if s.Config != nil { 132 133 if len(s.Config) > 1 { 134 return false, errors.Errorf("only one config key allowed per condition but %v provided", len(s.Config)) 135 } 136 137 // for loop will only cover first entry since we throw an error in case there is more than one config key defined already above 138 for param, activationValues := range s.Config { 139 for _, activationValue := range activationValues { 140 if activationValue == config.Config[param] { 141 return true, nil 142 } 143 } 144 return false, nil 145 } 146 } 147 148 if len(s.ConfigKey) > 0 { 149 configKey := strings.Split(s.ConfigKey, "/") 150 return checkConfigKeyV1(config.Config, configKey) 151 } 152 153 if len(s.FilePattern) > 0 { 154 files, err := utils.Glob(s.FilePattern) 155 if err != nil { 156 return false, errors.Wrap(err, "failed to check filePattern condition") 157 } 158 if len(files) > 0 { 159 return true, nil 160 } 161 return false, nil 162 } 163 164 if len(s.FilePatternFromConfig) > 0 { 165 166 configValue := fmt.Sprint(config.Config[s.FilePatternFromConfig]) 167 if len(configValue) == 0 { 168 return false, nil 169 } 170 files, err := utils.Glob(configValue) 171 if err != nil { 172 return false, errors.Wrap(err, "failed to check filePatternFromConfig condition") 173 } 174 if len(files) > 0 { 175 return true, nil 176 } 177 return false, nil 178 } 179 180 if len(s.NpmScript) > 0 { 181 return checkForNpmScriptsInPackagesV1(s.NpmScript, config, utils) 182 } 183 184 if s.CommonPipelineEnvironment != nil { 185 186 var metadata StepData 187 for param, value := range s.CommonPipelineEnvironment { 188 cpeEntry := getCPEEntry(param, value, &metadata, stepName, envRootPath) 189 if cpeEntry[stepName] == value { 190 return true, nil 191 } 192 } 193 return false, nil 194 } 195 196 if len(s.PipelineEnvironmentFilled) > 0 { 197 198 var metadata StepData 199 param := s.PipelineEnvironmentFilled 200 // check CPE for both a string and non-string value 201 cpeEntry := getCPEEntry(param, "", &metadata, stepName, envRootPath) 202 if len(cpeEntry) == 0 { 203 cpeEntry = getCPEEntry(param, nil, &metadata, stepName, envRootPath) 204 } 205 206 if _, ok := cpeEntry[stepName]; ok { 207 return true, nil 208 } 209 210 return false, nil 211 } 212 213 if s.OnlyActiveStepInStage { 214 // Used only in NotActiveConditions. 215 // Returns true if all other steps are inactive, so step will be deactivated 216 // if it's the only active step in stage. 217 // For example, sapCumulusUpload step must be deactivated in a stage where others steps are inactive. 218 return !anyOtherStepIsActive(stepName, runSteps), nil 219 } 220 221 // needs to be checked last: 222 // if none of the other conditions matches, step will be active unless set to inactive 223 if s.Inactive == true { 224 return false, nil 225 } else { 226 return true, nil 227 } 228 } 229 230 func getCPEEntry(param string, value interface{}, metadata *StepData, stepName string, envRootPath string) map[string]interface{} { 231 dataType := "interface" 232 _, ok := value.(string) 233 if ok { 234 dataType = "string" 235 } 236 metadata.Spec.Inputs.Parameters = []StepParameters{ 237 {Name: stepName, 238 Type: dataType, 239 ResourceRef: []ResourceReference{{Name: "commonPipelineEnvironment", Param: param}}, 240 }, 241 } 242 return metadata.GetResourceParameters(envRootPath, "commonPipelineEnvironment") 243 } 244 245 func checkConfigKeyV1(config map[string]interface{}, configKey []string) (bool, error) { 246 value, ok := config[configKey[0]] 247 if len(configKey) == 1 { 248 return ok, nil 249 } 250 castedValue, ok := value.(map[string]interface{}) 251 if !ok { 252 return false, nil 253 } 254 return checkConfigKeyV1(castedValue, configKey[1:]) 255 } 256 257 // EvaluateConditions validates stage conditions and updates runSteps in runConfig 258 func (r *RunConfig) evaluateConditions(config *Config, filters map[string]StepFilters, parameters map[string][]StepParameters, 259 secrets map[string][]StepSecrets, stepAliases map[string][]Alias, glob func(pattern string) (matches []string, err error)) error { 260 for stageName, stepConditions := range r.StageConfig.Stages { 261 runStep := map[string]bool{} 262 for stepName, stepCondition := range stepConditions.Conditions { 263 stepActive := false 264 stepConfig, err := r.getStepConfig(config, stageName, stepName, filters, parameters, secrets, stepAliases) 265 if err != nil { 266 return err 267 } 268 269 if active, ok := stepConfig.Config[stepName].(bool); ok { 270 // respect explicit activation/de-activation if available 271 stepActive = active 272 } else { 273 for conditionName, condition := range stepCondition { 274 var err error 275 switch conditionName { 276 case configCondition: 277 if stepActive, err = checkConfig(condition, stepConfig, stepName); err != nil { 278 return errors.Wrapf(err, "error: check config condition failed") 279 } 280 case configKeysCondition: 281 if stepActive, err = checkConfigKeys(condition, stepConfig, stepName); err != nil { 282 return errors.Wrapf(err, "error: check configKeys condition failed") 283 } 284 case filePatternFromConfigCondition: 285 if stepActive, err = checkForFilesWithPatternFromConfig(condition, stepConfig, stepName, glob); err != nil { 286 return errors.Wrapf(err, "error: check filePatternFromConfig condition failed") 287 } 288 case filePatternCondition: 289 if stepActive, err = checkForFilesWithPattern(condition, stepConfig, stepName, glob); err != nil { 290 return errors.Wrapf(err, "error: check filePattern condition failed") 291 } 292 case npmScriptsCondition: 293 if stepActive, err = checkForNpmScriptsInPackages(condition, stepConfig, stepName, glob, r.OpenFile); err != nil { 294 return errors.Wrapf(err, "error: check npmScripts condition failed") 295 } 296 default: 297 return errors.Errorf("unknown condition %s", conditionName) 298 } 299 if stepActive { 300 break 301 } 302 } 303 } 304 runStep[stepName] = stepActive 305 r.RunSteps[stageName] = runStep 306 } 307 } 308 return nil 309 } 310 311 func checkConfig(condition interface{}, config StepConfig, stepName string) (bool, error) { 312 switch condition := condition.(type) { 313 case string: 314 if configValue := stepConfigLookup(config.Config, stepName, condition); configValue != nil { 315 return true, nil 316 } 317 case map[string]interface{}: 318 for conditionConfigKey, conditionConfigValue := range condition { 319 configValue := stepConfigLookup(config.Config, stepName, conditionConfigKey) 320 if configValue == nil { 321 return false, nil 322 } 323 configValueStr, ok := configValue.(string) 324 if !ok { 325 return false, errors.Errorf("error: config value of %v to compare with is not a string", configValue) 326 } 327 condConfigValueArr, ok := conditionConfigValue.([]interface{}) 328 if !ok { 329 return false, errors.Errorf("error: type assertion to []interface{} failed: %T", conditionConfigValue) 330 } 331 for _, item := range condConfigValueArr { 332 itemStr, ok := item.(string) 333 if !ok { 334 return false, errors.Errorf("error: type assertion to string failed: %T", conditionConfigValue) 335 } 336 if configValueStr == itemStr { 337 return true, nil 338 } 339 } 340 } 341 default: 342 return false, errors.Errorf("error: condidiion type invalid: %T, possible types: string, map[string]interface{}", condition) 343 } 344 345 return false, nil 346 } 347 348 func checkConfigKey(configKey string, config StepConfig, stepName string) (bool, error) { 349 if configValue := stepConfigLookup(config.Config, stepName, configKey); configValue != nil { 350 return true, nil 351 } 352 return false, nil 353 } 354 355 func checkConfigKeys(condition interface{}, config StepConfig, stepName string) (bool, error) { 356 arrCondition, ok := condition.([]interface{}) 357 if !ok { 358 return false, errors.Errorf("error: type assertion to []interface{} failed: %T", condition) 359 } 360 for _, configKey := range arrCondition { 361 if configValue := stepConfigLookup(config.Config, stepName, configKey.(string)); configValue != nil { 362 return true, nil 363 } 364 } 365 return false, nil 366 } 367 368 func checkForFilesWithPatternFromConfig(condition interface{}, config StepConfig, stepName string, 369 glob func(pattern string) (matches []string, err error)) (bool, error) { 370 filePatternConfig, ok := condition.(string) 371 if !ok { 372 return false, errors.Errorf("error: type assertion to string failed: %T", condition) 373 } 374 filePatternFromConfig := stepConfigLookup(config.Config, stepName, filePatternConfig) 375 if filePatternFromConfig == nil { 376 return false, nil 377 } 378 filePattern, ok := filePatternFromConfig.(string) 379 if !ok { 380 return false, errors.Errorf("error: type assertion to string failed: %T", filePatternFromConfig) 381 } 382 matches, err := glob(filePattern) 383 if err != nil { 384 return false, errors.Wrap(err, "error: failed to check if file-exists") 385 } 386 if len(matches) > 0 { 387 return true, nil 388 } 389 return false, nil 390 } 391 392 func checkForFilesWithPattern(condition interface{}, config StepConfig, stepName string, 393 glob func(pattern string) (matches []string, err error)) (bool, error) { 394 switch condition := condition.(type) { 395 case string: 396 filePattern := condition 397 matches, err := glob(filePattern) 398 if err != nil { 399 return false, errors.Wrap(err, "error: failed to check if file-exists") 400 } 401 if len(matches) > 0 { 402 return true, nil 403 } 404 case []interface{}: 405 filePatterns := condition 406 for _, filePattern := range filePatterns { 407 filePatternStr, ok := filePattern.(string) 408 if !ok { 409 return false, errors.Errorf("error: type assertion to string failed: %T", filePatternStr) 410 } 411 matches, err := glob(filePatternStr) 412 if err != nil { 413 return false, errors.Wrap(err, "error: failed to check if file-exists") 414 } 415 if len(matches) > 0 { 416 return true, nil 417 } 418 } 419 default: 420 return false, errors.Errorf("error: condidiion type invalid: %T, possible types: string, []interface{}", condition) 421 } 422 return false, nil 423 } 424 425 func checkForNpmScriptsInPackages(condition interface{}, config StepConfig, stepName string, 426 glob func(pattern string) (matches []string, err error), openFile func(s string, t map[string]string) (io.ReadCloser, error)) (bool, error) { 427 packages, err := glob("**/package.json") 428 if err != nil { 429 return false, errors.Wrap(err, "error: failed to check if file-exists") 430 } 431 for _, pack := range packages { 432 packDirs := strings.Split(path.Dir(pack), "/") 433 isNodeModules := false 434 for _, dir := range packDirs { 435 if dir == "node_modules" { 436 isNodeModules = true 437 break 438 } 439 } 440 if isNodeModules { 441 continue 442 } 443 444 jsonFile, err := openFile(pack, nil) 445 if err != nil { 446 return false, errors.Errorf("error: failed to open file %s: %v", pack, err) 447 } 448 defer jsonFile.Close() 449 packageJSON := map[string]interface{}{} 450 if err := json.NewDecoder(jsonFile).Decode(&packageJSON); err != nil { 451 return false, errors.Errorf("error: failed to unmarshal json file %s: %v", pack, err) 452 } 453 npmScripts, ok := packageJSON["scripts"] 454 if !ok { 455 continue 456 } 457 scriptsMap, ok := npmScripts.(map[string]interface{}) 458 if !ok { 459 return false, errors.Errorf("error: type assertion to map[string]interface{} failed: %T", npmScripts) 460 } 461 switch condition := condition.(type) { 462 case string: 463 if _, ok := scriptsMap[condition]; ok { 464 return true, nil 465 } 466 case []interface{}: 467 for _, conditionNpmScript := range condition { 468 conditionNpmScriptStr, ok := conditionNpmScript.(string) 469 if !ok { 470 return false, errors.Errorf("error: type assertion to string failed: %T", conditionNpmScript) 471 } 472 if _, ok := scriptsMap[conditionNpmScriptStr]; ok { 473 return true, nil 474 } 475 } 476 default: 477 return false, errors.Errorf("error: condidiion type invalid: %T, possible types: string, []interface{}", condition) 478 } 479 } 480 return false, nil 481 } 482 483 func checkForNpmScriptsInPackagesV1(npmScript string, config StepConfig, utils piperutils.FileUtils) (bool, error) { 484 packages, err := utils.Glob("**/package.json") 485 if err != nil { 486 return false, errors.Wrap(err, "failed to check if file-exists") 487 } 488 for _, pack := range packages { 489 packDirs := strings.Split(path.Dir(pack), "/") 490 isNodeModules := false 491 for _, dir := range packDirs { 492 if dir == "node_modules" { 493 isNodeModules = true 494 break 495 } 496 } 497 if isNodeModules { 498 continue 499 } 500 501 jsonFile, err := utils.FileRead(pack) 502 if err != nil { 503 return false, errors.Errorf("failed to open file %s: %v", pack, err) 504 } 505 packageJSON := map[string]interface{}{} 506 if err := json.Unmarshal(jsonFile, &packageJSON); err != nil { 507 return false, errors.Errorf("failed to unmarshal json file %s: %v", pack, err) 508 } 509 npmScripts, ok := packageJSON["scripts"] 510 if !ok { 511 continue 512 } 513 scriptsMap, ok := npmScripts.(map[string]interface{}) 514 if !ok { 515 return false, errors.Errorf("failed to read scripts from package.json: %T", npmScripts) 516 } 517 if _, ok := scriptsMap[npmScript]; ok { 518 return true, nil 519 } 520 } 521 return false, nil 522 } 523 524 // anyOtherStepIsActive loops through previous steps active states and returns true 525 // if at least one of them is active, otherwise result is false. Ignores the step that is being checked. 526 func anyOtherStepIsActive(targetStep string, runSteps map[string]bool) bool { 527 for step, isActive := range runSteps { 528 if isActive && step != targetStep { 529 return true 530 } 531 } 532 533 return false 534 }