-
Notifications
You must be signed in to change notification settings - Fork 582
/
evaluation.go
307 lines (267 loc) · 9.07 KB
/
evaluation.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
package config
import (
"encoding/json"
"fmt"
"path"
"strings"
"github.com/pkg/errors"
"github.com/SAP/jenkins-library/pkg/orchestrator"
"github.com/SAP/jenkins-library/pkg/piperutils"
)
const (
configCondition = "config"
configKeysCondition = "configKeys"
filePatternFromConfigCondition = "filePatternFromConfig"
filePatternCondition = "filePattern"
npmScriptsCondition = "npmScripts"
)
// evaluateConditionsV1 validates stage conditions and updates runSteps in runConfig according to V1 schema.
// Priority of step activation/deactivation is follow:
// - stepNotActiveCondition (highest, if any)
// - explicit activation/deactivation (medium, if any)
// - stepActiveConditions (lowest, step is active by default if no conditions are configured)
func (r *RunConfigV1) evaluateConditionsV1(config *Config, utils piperutils.FileUtils, envRootPath string) error {
if r.RunSteps == nil {
r.RunSteps = make(map[string]map[string]bool, len(r.PipelineConfig.Spec.Stages))
}
if r.RunStages == nil {
r.RunStages = make(map[string]bool, len(r.PipelineConfig.Spec.Stages))
}
currentOrchestrator := orchestrator.DetectOrchestrator().String()
for _, stage := range r.PipelineConfig.Spec.Stages {
// Currently, the displayName is being used, but it may be necessary
// to also consider using the technical name.
stageName := stage.DisplayName
// Check #1: Apply explicit activation/deactivation from config file (if any)
// and then evaluate stepActive conditions
runStep := make(map[string]bool, len(stage.Steps))
stepConfigCache := make(map[string]StepConfig, len(stage.Steps))
for _, step := range stage.Steps {
// Consider only orchestrator-specific steps if the orchestrator limitation is set.
if len(step.Orchestrators) > 0 && !piperutils.ContainsString(step.Orchestrators, currentOrchestrator) {
continue
}
stepConfig, err := r.getStepConfig(config, stageName, step.Name, nil, nil, nil, nil)
if err != nil {
return err
}
stepConfigCache[step.Name] = stepConfig
// Respect explicit activation/deactivation if available.
// Note that this has higher priority than step conditions
if active, ok := stepConfig.Config[step.Name].(bool); ok {
runStep[step.Name] = active
continue
}
// If no condition is available, the step will be active by default.
stepActive := true
for _, condition := range step.Conditions {
stepActive, err = condition.evaluateV1(stepConfig, utils, step.Name, envRootPath, runStep)
if err != nil {
return fmt.Errorf("failed to evaluate step conditions: %w", err)
}
if stepActive {
// The first condition that matches will be considered to activate the step.
break
}
}
runStep[step.Name] = stepActive
}
// Check #2: Evaluate stepNotActive conditions (if any) and deactivate the step if the condition is met.
//
// TODO: PART 1 : if explicit activation/de-activation is available should notActiveConditions be checked ?
// 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??
for _, step := range stage.Steps {
stepConfig, found := stepConfigCache[step.Name]
if !found {
// If no stepConfig exists here, it means that this step was skipped in previous checks.
continue
}
for _, condition := range step.NotActiveConditions {
stepNotActive, err := condition.evaluateV1(stepConfig, utils, step.Name, envRootPath, runStep)
if err != nil {
return fmt.Errorf("failed to evaluate not active step conditions: %w", err)
}
// Deactivate the step if the notActive condition is met.
if stepNotActive {
runStep[step.Name] = false
break
}
}
}
r.RunSteps[stageName] = runStep
stageActive := false
for _, anyStepIsActive := range r.RunSteps[stageName] {
if anyStepIsActive {
stageActive = true
}
}
r.RunStages[stageName] = stageActive
}
return nil
}
func (s *StepCondition) evaluateV1(
config StepConfig,
utils piperutils.FileUtils,
stepName string,
envRootPath string,
runSteps map[string]bool,
) (bool, error) {
// only the first condition will be evaluated.
// if multiple conditions should be checked they need to provided via the Conditions list
if s.Config != nil {
if len(s.Config) > 1 {
return false, errors.Errorf("only one config key allowed per condition but %v provided", len(s.Config))
}
// for loop will only cover first entry since we throw an error in case there is more than one config key defined already above
for param, activationValues := range s.Config {
for _, activationValue := range activationValues {
if activationValue == config.Config[param] {
return true, nil
}
}
return false, nil
}
}
if len(s.ConfigKey) > 0 {
configKey := strings.Split(s.ConfigKey, "/")
return checkConfigKeyV1(config.Config, configKey)
}
if len(s.FilePattern) > 0 {
files, err := utils.Glob(s.FilePattern)
if err != nil {
return false, errors.Wrap(err, "failed to check filePattern condition")
}
if len(files) > 0 {
return true, nil
}
return false, nil
}
if len(s.FilePatternFromConfig) > 0 {
configValue := fmt.Sprint(config.Config[s.FilePatternFromConfig])
if len(configValue) == 0 {
return false, nil
}
files, err := utils.Glob(configValue)
if err != nil {
return false, errors.Wrap(err, "failed to check filePatternFromConfig condition")
}
if len(files) > 0 {
return true, nil
}
return false, nil
}
if len(s.NpmScript) > 0 {
return checkForNpmScriptsInPackagesV1(s.NpmScript, config, utils)
}
if s.CommonPipelineEnvironment != nil {
var metadata StepData
for param, value := range s.CommonPipelineEnvironment {
cpeEntry := getCPEEntry(param, value, &metadata, stepName, envRootPath)
if cpeEntry[stepName] == value {
return true, nil
}
}
return false, nil
}
if len(s.PipelineEnvironmentFilled) > 0 {
var metadata StepData
param := s.PipelineEnvironmentFilled
// check CPE for both a string and non-string value
cpeEntry := getCPEEntry(param, "", &metadata, stepName, envRootPath)
if len(cpeEntry) == 0 {
cpeEntry = getCPEEntry(param, nil, &metadata, stepName, envRootPath)
}
if _, ok := cpeEntry[stepName]; ok {
return true, nil
}
return false, nil
}
if s.OnlyActiveStepInStage {
// Used only in NotActiveConditions.
// Returns true if all other steps are inactive, so step will be deactivated
// if it's the only active step in stage.
// For example, sapCumulusUpload step must be deactivated in a stage where others steps are inactive.
return !anyOtherStepIsActive(stepName, runSteps), nil
}
// needs to be checked last:
// if none of the other conditions matches, step will be active unless set to inactive
if s.Inactive == true {
return false, nil
} else {
return true, nil
}
}
func getCPEEntry(param string, value interface{}, metadata *StepData, stepName string, envRootPath string) map[string]interface{} {
dataType := "interface"
_, ok := value.(string)
if ok {
dataType = "string"
}
metadata.Spec.Inputs.Parameters = []StepParameters{
{Name: stepName,
Type: dataType,
ResourceRef: []ResourceReference{{Name: "commonPipelineEnvironment", Param: param}},
},
}
return metadata.GetResourceParameters(envRootPath, "commonPipelineEnvironment")
}
func checkConfigKeyV1(config map[string]interface{}, configKey []string) (bool, error) {
value, ok := config[configKey[0]]
if len(configKey) == 1 {
return ok, nil
}
castedValue, ok := value.(map[string]interface{})
if !ok {
return false, nil
}
return checkConfigKeyV1(castedValue, configKey[1:])
}
func checkForNpmScriptsInPackagesV1(npmScript string, config StepConfig, utils piperutils.FileUtils) (bool, error) {
packages, err := utils.Glob("**/package.json")
if err != nil {
return false, errors.Wrap(err, "failed to check if file-exists")
}
for _, pack := range packages {
packDirs := strings.Split(path.Dir(pack), "/")
isNodeModules := false
for _, dir := range packDirs {
if dir == "node_modules" {
isNodeModules = true
break
}
}
if isNodeModules {
continue
}
jsonFile, err := utils.FileRead(pack)
if err != nil {
return false, errors.Errorf("failed to open file %s: %v", pack, err)
}
packageJSON := map[string]interface{}{}
if err := json.Unmarshal(jsonFile, &packageJSON); err != nil {
return false, errors.Errorf("failed to unmarshal json file %s: %v", pack, err)
}
npmScripts, ok := packageJSON["scripts"]
if !ok {
continue
}
scriptsMap, ok := npmScripts.(map[string]interface{})
if !ok {
return false, errors.Errorf("failed to read scripts from package.json: %T", npmScripts)
}
if _, ok := scriptsMap[npmScript]; ok {
return true, nil
}
}
return false, nil
}
// anyOtherStepIsActive loops through previous steps active states and returns true
// if at least one of them is active, otherwise result is false. Ignores the step that is being checked.
func anyOtherStepIsActive(targetStep string, runSteps map[string]bool) bool {
for step, isActive := range runSteps {
if isActive && step != targetStep {
return true
}
}
return false
}