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), &params)
   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  }