mirror of
https://github.com/SAP/jenkins-library.git
synced 2024-12-14 11:03:09 +02:00
0f33f7a2ef
* Consider custom defaults in config.yml
274 lines
7.7 KiB
Go
274 lines
7.7 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/ghodss/yaml"
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// Config defines the structure of the config files
|
|
type Config struct {
|
|
CustomDefaults []string `json:"customDefaults,omitempty"`
|
|
General map[string]interface{} `json:"general"`
|
|
Stages map[string]map[string]interface{} `json:"stages"`
|
|
Steps map[string]map[string]interface{} `json:"steps"`
|
|
openFile func(s string) (io.ReadCloser, error)
|
|
}
|
|
|
|
// StepConfig defines the structure for merged step configuration
|
|
type StepConfig struct {
|
|
Config map[string]interface{}
|
|
}
|
|
|
|
// ReadConfig loads config and returns its content
|
|
func (c *Config) ReadConfig(configuration io.ReadCloser) error {
|
|
defer configuration.Close()
|
|
|
|
content, err := ioutil.ReadAll(configuration)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "error reading %v", configuration)
|
|
}
|
|
|
|
err = yaml.Unmarshal(content, &c)
|
|
if err != nil {
|
|
return NewParseError(fmt.Sprintf("error unmarshalling %q: %v", content, err))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ApplyAliasConfig adds configuration values available on aliases to primary configuration parameters
|
|
func (c *Config) ApplyAliasConfig(parameters []StepParameters, filters StepFilters, stageName, stepName string) {
|
|
for _, p := range parameters {
|
|
c.General = setParamValueFromAlias(c.General, filters.General, p)
|
|
if c.Stages[stageName] != nil {
|
|
c.Stages[stageName] = setParamValueFromAlias(c.Stages[stageName], filters.Stages, p)
|
|
}
|
|
if c.Steps[stepName] != nil {
|
|
c.Steps[stepName] = setParamValueFromAlias(c.Steps[stepName], filters.Steps, p)
|
|
}
|
|
}
|
|
}
|
|
|
|
func setParamValueFromAlias(configMap map[string]interface{}, filter []string, p StepParameters) map[string]interface{} {
|
|
if configMap != nil && configMap[p.Name] == nil && sliceContains(filter, p.Name) {
|
|
for _, a := range p.Aliases {
|
|
configMap[p.Name] = getDeepAliasValue(configMap, a.Name)
|
|
if configMap[p.Name] != nil {
|
|
return configMap
|
|
}
|
|
}
|
|
}
|
|
return configMap
|
|
}
|
|
|
|
func getDeepAliasValue(configMap map[string]interface{}, key string) interface{} {
|
|
parts := strings.Split(key, "/")
|
|
if len(parts) > 1 {
|
|
if configMap[parts[0]] == nil {
|
|
return nil
|
|
}
|
|
return getDeepAliasValue(configMap[parts[0]].(map[string]interface{}), strings.Join(parts[1:], "/"))
|
|
}
|
|
return configMap[key]
|
|
}
|
|
|
|
// GetStepConfig provides merged step configuration using defaults, config, if available
|
|
func (c *Config) GetStepConfig(flagValues map[string]interface{}, paramJSON string, configuration io.ReadCloser, defaults []io.ReadCloser, filters StepFilters, parameters []StepParameters, stageName, stepName string) (StepConfig, error) {
|
|
var stepConfig StepConfig
|
|
var d PipelineDefaults
|
|
|
|
if configuration != nil {
|
|
if err := c.ReadConfig(configuration); err != nil {
|
|
return StepConfig{}, errors.Wrap(err, "failed to parse custom pipeline configuration")
|
|
}
|
|
}
|
|
c.ApplyAliasConfig(parameters, filters, stageName, stepName)
|
|
|
|
// consider custom defaults defined in config.yml
|
|
if c.CustomDefaults != nil && len(c.CustomDefaults) > 0 {
|
|
if c.openFile == nil {
|
|
c.openFile = OpenPiperFile
|
|
}
|
|
for _, f := range c.CustomDefaults {
|
|
fc, err := c.openFile(f)
|
|
if err != nil {
|
|
return StepConfig{}, errors.Wrapf(err, "getting default '%v' failed", f)
|
|
}
|
|
defaults = append(defaults, fc)
|
|
}
|
|
}
|
|
|
|
if err := d.ReadPipelineDefaults(defaults); err != nil {
|
|
switch err.(type) {
|
|
case *ParseError:
|
|
return StepConfig{}, errors.Wrap(err, "failed to parse pipeline default configuration")
|
|
default:
|
|
//ignoring unavailability of defaults since considered optional
|
|
}
|
|
}
|
|
|
|
// first: read defaults & merge general -> steps (-> general -> steps ...)
|
|
for _, def := range d.Defaults {
|
|
def.ApplyAliasConfig(parameters, filters, stageName, stepName)
|
|
stepConfig.mixIn(def.General, filters.General)
|
|
stepConfig.mixIn(def.Steps[stepName], filters.Steps)
|
|
}
|
|
|
|
// second: read config & merge - general -> steps -> stages
|
|
stepConfig.mixIn(c.General, filters.General)
|
|
stepConfig.mixIn(c.Steps[stepName], filters.Steps)
|
|
stepConfig.mixIn(c.Stages[stageName], filters.Stages)
|
|
|
|
// third: merge parameters provided via env vars
|
|
stepConfig.mixIn(envValues(filters.All), filters.All)
|
|
|
|
// fourth: if parameters are provided in JSON format merge them
|
|
if len(paramJSON) != 0 {
|
|
var params map[string]interface{}
|
|
json.Unmarshal([]byte(paramJSON), ¶ms)
|
|
|
|
//apply aliases
|
|
for _, p := range parameters {
|
|
params = setParamValueFromAlias(params, filters.Parameters, p)
|
|
}
|
|
|
|
stepConfig.mixIn(params, filters.Parameters)
|
|
}
|
|
|
|
// fifth: merge command line flags
|
|
if flagValues != nil {
|
|
stepConfig.mixIn(flagValues, filters.Parameters)
|
|
}
|
|
|
|
// finally do the condition evaluation post processing
|
|
for _, p := range parameters {
|
|
if len(p.Conditions) > 0 {
|
|
cp := p.Conditions[0].Params[0]
|
|
dependentValue := stepConfig.Config[cp.Name]
|
|
if cmp.Equal(dependentValue, cp.Value) && stepConfig.Config[p.Name] == nil {
|
|
subMapValue := stepConfig.Config[dependentValue.(string)].(map[string]interface{})[p.Name]
|
|
if subMapValue != nil {
|
|
stepConfig.Config[p.Name] = subMapValue
|
|
} else {
|
|
stepConfig.Config[p.Name] = p.Default
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return stepConfig, nil
|
|
}
|
|
|
|
// GetStepConfigWithJSON provides merged step configuration using a provided stepConfigJSON with additional flags provided
|
|
func GetStepConfigWithJSON(flagValues map[string]interface{}, stepConfigJSON string, filters StepFilters) StepConfig {
|
|
var stepConfig StepConfig
|
|
|
|
stepConfigMap := map[string]interface{}{}
|
|
|
|
json.Unmarshal([]byte(stepConfigJSON), &stepConfigMap)
|
|
|
|
stepConfig.mixIn(stepConfigMap, filters.All)
|
|
|
|
// ToDo: mix in parametersJSON
|
|
|
|
if flagValues != nil {
|
|
stepConfig.mixIn(flagValues, filters.Parameters)
|
|
}
|
|
return stepConfig
|
|
}
|
|
|
|
// GetJSON returns JSON representation of an object
|
|
func GetJSON(data interface{}) (string, error) {
|
|
|
|
result, err := json.Marshal(data)
|
|
if err != nil {
|
|
return "", errors.Wrapf(err, "error marshalling json: %v", err)
|
|
}
|
|
return string(result), nil
|
|
}
|
|
|
|
// OpenPiperFile provides functionality to retrieve configuration via file or http
|
|
func OpenPiperFile(name string) (io.ReadCloser, error) {
|
|
//ToDo: support also https as source
|
|
if !strings.HasPrefix(name, "http") {
|
|
return os.Open(name)
|
|
}
|
|
return nil, fmt.Errorf("file location not yet supported for '%v'", name)
|
|
}
|
|
|
|
func envValues(filter []string) map[string]interface{} {
|
|
vals := map[string]interface{}{}
|
|
for _, param := range filter {
|
|
if envVal := os.Getenv("PIPER_" + param); len(envVal) != 0 {
|
|
vals[param] = os.Getenv("PIPER_" + param)
|
|
}
|
|
}
|
|
return vals
|
|
}
|
|
|
|
func (s *StepConfig) mixIn(mergeData map[string]interface{}, filter []string) {
|
|
|
|
if s.Config == nil {
|
|
s.Config = map[string]interface{}{}
|
|
}
|
|
|
|
s.Config = merge(s.Config, filterMap(mergeData, filter))
|
|
}
|
|
|
|
func filterMap(data map[string]interface{}, filter []string) map[string]interface{} {
|
|
result := map[string]interface{}{}
|
|
|
|
if data == nil {
|
|
data = map[string]interface{}{}
|
|
}
|
|
|
|
for key, value := range data {
|
|
if len(filter) == 0 || sliceContains(filter, key) {
|
|
result[key] = value
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func merge(base, overlay map[string]interface{}) map[string]interface{} {
|
|
|
|
result := map[string]interface{}{}
|
|
|
|
if base == nil {
|
|
base = map[string]interface{}{}
|
|
}
|
|
|
|
for key, value := range base {
|
|
result[key] = value
|
|
}
|
|
|
|
for key, value := range overlay {
|
|
if val, ok := value.(map[string]interface{}); ok {
|
|
if valBaseKey, ok := base[key].(map[string]interface{}); !ok {
|
|
result[key] = merge(map[string]interface{}{}, val)
|
|
} else {
|
|
result[key] = merge(valBaseKey, val)
|
|
}
|
|
} else {
|
|
result[key] = value
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func sliceContains(slice []string, find string) bool {
|
|
for _, elem := range slice {
|
|
if elem == find {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|