github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/pkg/config/config.go (about) 1 package config 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "net/http" 8 "net/url" 9 "os" 10 "reflect" 11 "regexp" 12 "strings" 13 14 piperhttp "github.com/SAP/jenkins-library/pkg/http" 15 "github.com/SAP/jenkins-library/pkg/log" 16 17 "github.com/ghodss/yaml" 18 "github.com/google/go-cmp/cmp" 19 "github.com/pkg/errors" 20 ) 21 22 // Config defines the structure of the config files 23 type Config struct { 24 CustomDefaults []string `json:"customDefaults,omitempty"` 25 General map[string]interface{} `json:"general"` 26 Stages map[string]map[string]interface{} `json:"stages"` 27 Steps map[string]map[string]interface{} `json:"steps"` 28 Hooks map[string]interface{} `json:"hooks,omitempty"` 29 defaults PipelineDefaults 30 initialized bool 31 accessTokens map[string]string 32 openFile func(s string, t map[string]string) (io.ReadCloser, error) 33 vaultCredentials VaultCredentials 34 } 35 36 // StepConfig defines the structure for merged step configuration 37 type StepConfig struct { 38 Config map[string]interface{} 39 HookConfig map[string]interface{} 40 } 41 42 // ReadConfig loads config and returns its content 43 func (c *Config) ReadConfig(configuration io.ReadCloser) error { 44 defer configuration.Close() 45 46 content, err := io.ReadAll(configuration) 47 if err != nil { 48 return errors.Wrapf(err, "error reading %v", configuration) 49 } 50 51 err = yaml.Unmarshal(content, &c) 52 if err != nil { 53 return NewParseError(fmt.Sprintf("format of configuration is invalid %q: %v", content, err)) 54 } 55 return nil 56 } 57 58 // ApplyAliasConfig adds configuration values available on aliases to primary configuration parameters 59 func (c *Config) ApplyAliasConfig(parameters []StepParameters, secrets []StepSecrets, filters StepFilters, stageName, stepName string, stepAliases []Alias) { 60 // copy configuration from step alias to correct step 61 if len(stepAliases) > 0 { 62 c.copyStepAliasConfig(stepName, stepAliases) 63 } 64 for _, p := range parameters { 65 c.General = setParamValueFromAlias(stepName, c.General, filters.General, p.Name, p.Aliases) 66 if c.Stages[stageName] != nil { 67 c.Stages[stageName] = setParamValueFromAlias(stepName, c.Stages[stageName], filters.Stages, p.Name, p.Aliases) 68 } 69 if c.Steps[stepName] != nil { 70 c.Steps[stepName] = setParamValueFromAlias(stepName, c.Steps[stepName], filters.Steps, p.Name, p.Aliases) 71 } 72 } 73 for _, s := range secrets { 74 c.General = setParamValueFromAlias(stepName, c.General, filters.General, s.Name, s.Aliases) 75 if c.Stages[stageName] != nil { 76 c.Stages[stageName] = setParamValueFromAlias(stepName, c.Stages[stageName], filters.Stages, s.Name, s.Aliases) 77 } 78 if c.Steps[stepName] != nil { 79 c.Steps[stepName] = setParamValueFromAlias(stepName, c.Steps[stepName], filters.Steps, s.Name, s.Aliases) 80 } 81 } 82 } 83 84 func setParamValueFromAlias(stepName string, configMap map[string]interface{}, filter []string, name string, aliases []Alias) map[string]interface{} { 85 if configMap != nil && configMap[name] == nil && sliceContains(filter, name) { 86 for _, a := range aliases { 87 aliasVal := getDeepAliasValue(configMap, a.Name) 88 if aliasVal != nil { 89 configMap[name] = aliasVal 90 if a.Deprecated { 91 log.Entry().Warningf("[WARNING] The parameter '%v' is DEPRECATED, use '%v' instead. (%v/%v)", a.Name, name, log.LibraryName, stepName) 92 } 93 } 94 if configMap[name] != nil { 95 return configMap 96 } 97 } 98 } 99 return configMap 100 } 101 102 func getDeepAliasValue(configMap map[string]interface{}, key string) interface{} { 103 parts := strings.Split(key, "/") 104 if len(parts) > 1 { 105 if configMap[parts[0]] == nil { 106 return nil 107 } 108 109 paramValueType := reflect.ValueOf(configMap[parts[0]]) 110 if paramValueType.Kind() != reflect.Map { 111 log.Entry().Debugf("Ignoring alias '%v' as '%v' is not pointing to a map.", key, parts[0]) 112 return nil 113 } 114 return getDeepAliasValue(configMap[parts[0]].(map[string]interface{}), strings.Join(parts[1:], "/")) 115 } 116 return configMap[key] 117 } 118 119 func (c *Config) copyStepAliasConfig(stepName string, stepAliases []Alias) { 120 for _, stepAlias := range stepAliases { 121 if c.Steps[stepAlias.Name] != nil { 122 if stepAlias.Deprecated { 123 log.Entry().WithField("package", "SAP/jenkins-library/pkg/config").Warningf("DEPRECATION NOTICE: step configuration available for deprecated step '%v'. Please remove or move configuration to step '%v'!", stepAlias.Name, stepName) 124 } 125 for paramName, paramValue := range c.Steps[stepAlias.Name] { 126 if c.Steps[stepName] == nil { 127 c.Steps[stepName] = map[string]interface{}{} 128 } 129 if c.Steps[stepName][paramName] == nil { 130 c.Steps[stepName][paramName] = paramValue 131 } 132 } 133 } 134 } 135 } 136 137 // InitializeConfig prepares the config object, i.e. loading content, etc. 138 func (c *Config) InitializeConfig(configuration io.ReadCloser, defaults []io.ReadCloser, ignoreCustomDefaults bool) error { 139 if configuration != nil { 140 if err := c.ReadConfig(configuration); err != nil { 141 return errors.Wrap(err, "failed to parse custom pipeline configuration") 142 } 143 } 144 145 // consider custom defaults defined in config.yml unless told otherwise 146 if ignoreCustomDefaults { 147 log.Entry().Debug("Ignoring custom defaults from pipeline config") 148 } else if c.CustomDefaults != nil && len(c.CustomDefaults) > 0 { 149 if c.openFile == nil { 150 c.openFile = OpenPiperFile 151 } 152 for _, f := range c.CustomDefaults { 153 fc, err := c.openFile(f, c.accessTokens) 154 if err != nil { 155 return errors.Wrapf(err, "getting default '%v' failed", f) 156 } 157 defaults = append(defaults, fc) 158 } 159 } 160 161 if err := c.defaults.ReadPipelineDefaults(defaults); err != nil { 162 return errors.Wrap(err, "failed to read default configuration") 163 } 164 c.initialized = true 165 return nil 166 } 167 168 // GetStepConfig provides merged step configuration using defaults, config, if available 169 func (c *Config) GetStepConfig(flagValues map[string]interface{}, paramJSON string, configuration io.ReadCloser, defaults []io.ReadCloser, ignoreCustomDefaults bool, filters StepFilters, metadata StepData, envParameters map[string]interface{}, stageName, stepName string) (StepConfig, error) { 170 parameters := metadata.Spec.Inputs.Parameters 171 secrets := metadata.Spec.Inputs.Secrets 172 stepAliases := metadata.Metadata.Aliases 173 174 var stepConfig StepConfig 175 var err error 176 177 if !c.initialized { 178 err = c.InitializeConfig(configuration, defaults, ignoreCustomDefaults) 179 if err != nil { 180 return StepConfig{}, err 181 } 182 } 183 184 c.ApplyAliasConfig(parameters, secrets, filters, stageName, stepName, stepAliases) 185 186 // initialize with defaults from step.yaml 187 stepConfig.mixInStepDefaults(parameters) 188 189 // merge parameters provided by Piper environment 190 stepConfig.mixIn(envParameters, filters.All) 191 stepConfig.mixIn(envParameters, ReportingParameters.getReportingFilter()) 192 193 // read defaults & merge general -> steps (-> general -> steps ...) 194 for _, def := range c.defaults.Defaults { 195 def.ApplyAliasConfig(parameters, secrets, filters, stageName, stepName, stepAliases) 196 stepConfig.mixIn(def.General, filters.General) 197 stepConfig.mixIn(def.Steps[stepName], filters.Steps) 198 stepConfig.mixIn(def.Stages[stageName], filters.Steps) 199 stepConfig.mixinVaultConfig(parameters, def.General, def.Steps[stepName], def.Stages[stageName]) 200 reportingConfig, err := cloneConfig(&def) 201 if err != nil { 202 return StepConfig{}, err 203 } 204 reportingConfig.ApplyAliasConfig(ReportingParameters.Parameters, []StepSecrets{}, ReportingParameters.getStepFilters(), stageName, stepName, []Alias{}) 205 stepConfig.mixinReportingConfig(reportingConfig.General, reportingConfig.Steps[stepName], reportingConfig.Stages[stageName]) 206 207 stepConfig.mixInHookConfig(def.Hooks) 208 } 209 210 // read config & merge - general -> steps -> stages 211 stepConfig.mixIn(c.General, filters.General) 212 stepConfig.mixIn(c.Steps[stepName], filters.Steps) 213 stepConfig.mixIn(c.Stages[stageName], filters.Stages) 214 215 // merge parameters provided via env vars 216 stepConfig.mixIn(envValues(filters.All), filters.All) 217 218 // if parameters are provided in JSON format merge them 219 if len(paramJSON) != 0 { 220 var params map[string]interface{} 221 err := json.Unmarshal([]byte(paramJSON), ¶ms) 222 if err != nil { 223 log.Entry().Warnf("failed to parse parameters from environment: %v", err) 224 } else { 225 // apply aliases 226 for _, p := range parameters { 227 params = setParamValueFromAlias(stepName, params, filters.Parameters, p.Name, p.Aliases) 228 } 229 for _, s := range secrets { 230 params = setParamValueFromAlias(stepName, params, filters.Parameters, s.Name, s.Aliases) 231 } 232 233 stepConfig.mixIn(params, filters.Parameters) 234 } 235 } 236 237 // merge command line flags 238 if flagValues != nil { 239 stepConfig.mixIn(flagValues, filters.Parameters) 240 } 241 242 if verbose, ok := stepConfig.Config["verbose"].(bool); ok && verbose { 243 log.SetVerbose(verbose) 244 } else if !ok && stepConfig.Config["verbose"] != nil { 245 log.Entry().Warnf("invalid value for parameter verbose: '%v'", stepConfig.Config["verbose"]) 246 } 247 248 stepConfig.mixinVaultConfig(parameters, c.General, c.Steps[stepName], c.Stages[stageName]) 249 250 reportingConfig, err := cloneConfig(c) 251 if err != nil { 252 return StepConfig{}, err 253 } 254 reportingConfig.ApplyAliasConfig(ReportingParameters.Parameters, []StepSecrets{}, ReportingParameters.getStepFilters(), stageName, stepName, []Alias{}) 255 stepConfig.mixinReportingConfig(reportingConfig.General, reportingConfig.Steps[stepName], reportingConfig.Stages[stageName]) 256 257 // check whether vault should be skipped 258 if skip, ok := stepConfig.Config["skipVault"].(bool); !ok || !skip { 259 // fetch secrets from vault 260 vaultClient, err := getVaultClientFromConfig(stepConfig, c.vaultCredentials) 261 if err != nil { 262 return StepConfig{}, err 263 } 264 if vaultClient != nil { 265 defer vaultClient.MustRevokeToken() 266 resolveAllVaultReferences(&stepConfig, vaultClient, append(parameters, ReportingParameters.Parameters...)) 267 resolveVaultTestCredentialsWrapper(&stepConfig, vaultClient) 268 resolveVaultCredentialsWrapper(&stepConfig, vaultClient) 269 } 270 } 271 272 // finally do the condition evaluation post processing 273 for _, p := range parameters { 274 if len(p.Conditions) > 0 { 275 for _, cond := range p.Conditions { 276 for _, param := range cond.Params { 277 // retrieve configuration value of condition parameter 278 dependentValue := stepConfig.Config[param.Name] 279 // check if configuration of condition parameter matches the value 280 // so far string-equals condition is assumed here 281 // if so and if no config applied yet, then try to apply the value 282 if cmp.Equal(dependentValue, param.Value) && stepConfig.Config[p.Name] == nil { 283 subMap, ok := stepConfig.Config[dependentValue.(string)].(map[string]interface{}) 284 if ok && subMap[p.Name] != nil { 285 stepConfig.Config[p.Name] = subMap[p.Name] 286 } 287 } 288 } 289 } 290 } 291 } 292 return stepConfig, nil 293 } 294 295 // SetVaultCredentials sets the appRoleID and the appRoleSecretID or the vaultTokento load additional 296 // configuration from vault 297 // Either appRoleID and appRoleSecretID or vaultToken must be specified. 298 func (c *Config) SetVaultCredentials(appRoleID, appRoleSecretID string, vaultToken string) { 299 c.vaultCredentials = VaultCredentials{ 300 AppRoleID: appRoleID, 301 AppRoleSecretID: appRoleSecretID, 302 VaultToken: vaultToken, 303 } 304 } 305 306 // GetStepConfigWithJSON provides merged step configuration using a provided stepConfigJSON with additional flags provided 307 func GetStepConfigWithJSON(flagValues map[string]interface{}, stepConfigJSON string, filters StepFilters) StepConfig { 308 var stepConfig StepConfig 309 310 stepConfigMap := map[string]interface{}{} 311 312 err := json.Unmarshal([]byte(stepConfigJSON), &stepConfigMap) 313 if err != nil { 314 log.Entry().Warnf("invalid stepConfig JSON: %v", err) 315 } 316 317 stepConfig.mixIn(stepConfigMap, filters.All) 318 319 // ToDo: mix in parametersJSON 320 321 if flagValues != nil { 322 stepConfig.mixIn(flagValues, filters.Parameters) 323 } 324 return stepConfig 325 } 326 327 func (c *Config) GetStageConfig(paramJSON string, configuration io.ReadCloser, defaults []io.ReadCloser, ignoreCustomDefaults bool, acceptedParams []string, stageName string) (StepConfig, error) { 328 329 filters := StepFilters{ 330 General: acceptedParams, 331 Steps: []string{}, 332 Stages: acceptedParams, 333 Parameters: acceptedParams, 334 Env: []string{}, 335 } 336 return c.GetStepConfig(map[string]interface{}{}, paramJSON, configuration, defaults, ignoreCustomDefaults, filters, StepData{}, map[string]interface{}{}, stageName, "") 337 } 338 339 // GetJSON returns JSON representation of an object 340 func GetJSON(data interface{}) (string, error) { 341 342 result, err := json.Marshal(data) 343 if err != nil { 344 return "", errors.Wrapf(err, "error marshalling json: %v", err) 345 } 346 return string(result), nil 347 } 348 349 // GetYAML returns YAML representation of an object 350 func GetYAML(data interface{}) (string, error) { 351 352 result, err := yaml.Marshal(data) 353 if err != nil { 354 return "", errors.Wrapf(err, "error marshalling yaml: %v", err) 355 } 356 return string(result), nil 357 } 358 359 // OpenPiperFile provides functionality to retrieve configuration via file or http 360 func OpenPiperFile(name string, accessTokens map[string]string) (io.ReadCloser, error) { 361 if len(name) == 0 { 362 return nil, errors.Wrap(os.ErrNotExist, "no filename provided") 363 } 364 365 if !strings.HasPrefix(name, "http://") && !strings.HasPrefix(name, "https://") { 366 return os.Open(name) 367 } 368 369 return httpReadFile(name, accessTokens) 370 } 371 372 func httpReadFile(name string, accessTokens map[string]string) (io.ReadCloser, error) { 373 374 u, err := url.Parse(name) 375 if err != nil { 376 return nil, fmt.Errorf("failed to read url: %w", err) 377 } 378 379 // support http(s) urls next to file path 380 client := piperhttp.Client{} 381 382 var header http.Header 383 if len(accessTokens[u.Host]) > 0 { 384 client.SetOptions(piperhttp.ClientOptions{Token: fmt.Sprintf("token %v", accessTokens[u.Host])}) 385 header = map[string][]string{"Accept": {"application/vnd.github.v3.raw"}} 386 } 387 388 response, err := client.SendRequest("GET", name, nil, header, nil) 389 if err != nil { 390 return nil, err 391 } 392 return response.Body, nil 393 } 394 395 func envValues(filter []string) map[string]interface{} { 396 vals := map[string]interface{}{} 397 for _, param := range filter { 398 if envVal := os.Getenv("PIPER_" + param); len(envVal) != 0 { 399 vals[param] = os.Getenv("PIPER_" + param) 400 } 401 } 402 return vals 403 } 404 405 func (s *StepConfig) mixIn(mergeData map[string]interface{}, filter []string) { 406 407 if s.Config == nil { 408 s.Config = map[string]interface{}{} 409 } 410 411 s.Config = merge(s.Config, filterMap(mergeData, filter)) 412 } 413 414 func (s *StepConfig) mixInHookConfig(mergeData map[string]interface{}) { 415 416 if s.HookConfig == nil { 417 s.HookConfig = map[string]interface{}{} 418 } 419 420 s.HookConfig = merge(s.HookConfig, mergeData) 421 } 422 423 func (s *StepConfig) mixInStepDefaults(stepParams []StepParameters) { 424 if s.Config == nil { 425 s.Config = map[string]interface{}{} 426 } 427 428 // conditional defaults need to be written to a sub map 429 // in order to prevent a "last one wins" situation 430 // this is then considered at the end of GetStepConfig once the complete configuration is known 431 for _, p := range stepParams { 432 if p.Default != nil { 433 if len(p.Conditions) == 0 { 434 s.Config[p.Name] = p.Default 435 } else { 436 for _, cond := range p.Conditions { 437 for _, param := range cond.Params { 438 s.Config[param.Value] = map[string]interface{}{p.Name: p.Default} 439 } 440 } 441 } 442 } 443 } 444 } 445 446 // ApplyContainerConditions evaluates conditions in step yaml container definitions 447 func ApplyContainerConditions(containers []Container, stepConfig *StepConfig) { 448 for _, container := range containers { 449 if len(container.Conditions) > 0 { 450 for _, param := range container.Conditions[0].Params { 451 if container.Conditions[0].ConditionRef == "strings-equal" && stepConfig.Config[param.Name] == param.Value { 452 var containerConf map[string]interface{} 453 if stepConfig.Config[param.Value] != nil { 454 containerConf = stepConfig.Config[param.Value].(map[string]interface{}) 455 for key, value := range containerConf { 456 if stepConfig.Config[key] == nil { 457 stepConfig.Config[key] = value 458 } 459 } 460 delete(stepConfig.Config, param.Value) 461 } 462 } 463 } 464 } 465 } 466 } 467 468 func filterMap(data map[string]interface{}, filter []string) map[string]interface{} { 469 result := map[string]interface{}{} 470 471 if data == nil { 472 data = map[string]interface{}{} 473 } 474 475 for key, value := range data { 476 if value != nil && (len(filter) == 0 || sliceContains(filter, key)) { 477 result[key] = value 478 } 479 } 480 return result 481 } 482 483 func merge(base, overlay map[string]interface{}) map[string]interface{} { 484 485 result := map[string]interface{}{} 486 487 if base == nil { 488 base = map[string]interface{}{} 489 } 490 491 for key, value := range base { 492 result[key] = value 493 } 494 495 for key, value := range overlay { 496 if val, ok := value.(map[string]interface{}); ok { 497 if valBaseKey, ok := base[key].(map[string]interface{}); !ok { 498 result[key] = merge(map[string]interface{}{}, val) 499 } else { 500 result[key] = merge(valBaseKey, val) 501 } 502 } else { 503 result[key] = value 504 } 505 } 506 return result 507 } 508 509 func sliceContains(slice []string, find string) bool { 510 for _, elem := range slice { 511 matches, _ := regexp.MatchString(elem, find) 512 if matches { 513 return true 514 } 515 } 516 return false 517 } 518 519 func cloneConfig(config *Config) (*Config, error) { 520 configJSON, err := json.Marshal(config) 521 if err != nil { 522 return nil, err 523 } 524 525 clone := &Config{} 526 if err = json.Unmarshal(configJSON, &clone); err != nil { 527 return nil, err 528 } 529 530 return clone, nil 531 }