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  }