github.com/xgoffin/jenkins-library@v1.154.0/cmd/getConfig.go (about)

     1  package cmd
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"os"
     7  	"path"
     8  	"path/filepath"
     9  
    10  	"github.com/SAP/jenkins-library/pkg/config"
    11  	"github.com/SAP/jenkins-library/pkg/log"
    12  	"github.com/SAP/jenkins-library/pkg/piperutils"
    13  	"github.com/SAP/jenkins-library/pkg/reporting"
    14  	ws "github.com/SAP/jenkins-library/pkg/whitesource"
    15  	"github.com/pkg/errors"
    16  	"github.com/spf13/cobra"
    17  )
    18  
    19  type configCommandOptions struct {
    20  	output                        string // output format, so far only JSON
    21  	outputFile                    string // if set: path to file where the output should be written to
    22  	parametersJSON                string // parameters to be considered in JSON format
    23  	stageConfig                   bool
    24  	stageConfigAcceptedParameters []string
    25  	stepMetadata                  string // metadata to be considered, can be filePath or ENV containing JSON in format 'ENV:MY_ENV_VAR'
    26  	stepName                      string
    27  	contextConfig                 bool
    28  	openFile                      func(s string, t map[string]string) (io.ReadCloser, error)
    29  }
    30  
    31  var configOptions configCommandOptions
    32  
    33  type getConfigUtils interface {
    34  	FileExists(filename string) (bool, error)
    35  	DirExists(path string) (bool, error)
    36  	FileWrite(path string, content []byte, perm os.FileMode) error
    37  }
    38  
    39  type getConfigUtilsBundle struct {
    40  	*piperutils.Files
    41  }
    42  
    43  func newGetConfigUtilsUtils() getConfigUtils {
    44  	utils := getConfigUtilsBundle{
    45  		Files: &piperutils.Files{},
    46  	}
    47  	return &utils
    48  }
    49  
    50  // ConfigCommand is the entry command for loading the configuration of a pipeline step
    51  func ConfigCommand() *cobra.Command {
    52  
    53  	configOptions.openFile = config.OpenPiperFile
    54  	var createConfigCmd = &cobra.Command{
    55  		Use:   "getConfig",
    56  		Short: "Loads the project 'Piper' configuration respecting defaults and parameters.",
    57  		PreRun: func(cmd *cobra.Command, args []string) {
    58  			path, _ := os.Getwd()
    59  			fatalHook := &log.FatalHook{CorrelationID: GeneralConfig.CorrelationID, Path: path}
    60  			log.RegisterHook(fatalHook)
    61  			initStageName(false)
    62  			GeneralConfig.GitHubAccessTokens = ResolveAccessTokens(GeneralConfig.GitHubTokens)
    63  		},
    64  		Run: func(cmd *cobra.Command, _ []string) {
    65  			utils := newGetConfigUtilsUtils()
    66  			err := generateConfig(utils)
    67  			if err != nil {
    68  				log.SetErrorCategory(log.ErrorConfiguration)
    69  				log.Entry().WithError(err).Fatal("failed to retrieve configuration")
    70  			}
    71  		},
    72  	}
    73  
    74  	addConfigFlags(createConfigCmd)
    75  	return createConfigCmd
    76  }
    77  
    78  // GetDockerImageValue provides Piper commands additional access to configuration of step execution image if required
    79  func GetDockerImageValue(stepName string) (string, error) {
    80  	configOptions.contextConfig = true
    81  	configOptions.stepName = stepName
    82  	stepConfig, err := getConfig()
    83  	if err != nil {
    84  		return "", err
    85  	}
    86  
    87  	var dockerImageValue string
    88  	dockerImageValue, ok := stepConfig.Config["dockerImage"].(string)
    89  	if !ok {
    90  		log.Entry().Infof("Config value of %v to compare with is not a string", stepConfig.Config["dockerImage"])
    91  	}
    92  
    93  	return dockerImageValue, nil
    94  }
    95  
    96  func getBuildToolFromStageConfig(stepName string) (string, error) {
    97  	configOptions.contextConfig = true
    98  	configOptions.stepName = stepName
    99  	stageConfig, err := GetStageConfig()
   100  	if err != nil {
   101  		return "", err
   102  	}
   103  
   104  	buildTool, ok := stageConfig.Config["buildTool"].(string)
   105  	if !ok {
   106  		log.Entry().Infof("Config value of %v to compare with is not a string", stageConfig.Config["buildTool"])
   107  	}
   108  
   109  	return buildTool, nil
   110  }
   111  
   112  // GetStageConfig provides Piper commands additional access to stage configuration if required.
   113  // This allows steps to refer to configuration parameters which are not part of the step itself.
   114  func GetStageConfig() (config.StepConfig, error) {
   115  	myConfig := config.Config{}
   116  	stepConfig := config.StepConfig{}
   117  	projectConfigFile := getProjectConfigFile(GeneralConfig.CustomConfig)
   118  
   119  	customConfig, err := configOptions.openFile(projectConfigFile, GeneralConfig.GitHubAccessTokens)
   120  	if err != nil {
   121  		if !errors.Is(err, os.ErrNotExist) {
   122  			return stepConfig, errors.Wrapf(err, "config: open configuration file '%v' failed", projectConfigFile)
   123  		}
   124  		customConfig = nil
   125  	}
   126  
   127  	defaultConfig := []io.ReadCloser{}
   128  	for _, f := range GeneralConfig.DefaultConfig {
   129  		fc, err := configOptions.openFile(f, GeneralConfig.GitHubAccessTokens)
   130  		// only create error for non-default values
   131  		if err != nil && f != ".pipeline/defaults.yaml" {
   132  			return stepConfig, errors.Wrapf(err, "config: getting defaults failed: '%v'", f)
   133  		}
   134  		if err == nil {
   135  			defaultConfig = append(defaultConfig, fc)
   136  		}
   137  	}
   138  
   139  	return myConfig.GetStageConfig(GeneralConfig.ParametersJSON, customConfig, defaultConfig, GeneralConfig.IgnoreCustomDefaults, configOptions.stageConfigAcceptedParameters, GeneralConfig.StageName)
   140  }
   141  
   142  func getConfig() (config.StepConfig, error) {
   143  	var myConfig config.Config
   144  	var stepConfig config.StepConfig
   145  	var err error
   146  
   147  	if configOptions.stageConfig {
   148  		stepConfig, err = GetStageConfig()
   149  		if err != nil {
   150  			return stepConfig, errors.Wrap(err, "getting stage config failed")
   151  		}
   152  	} else {
   153  		log.Entry().Infof("Printing stepName %s", configOptions.stepName)
   154  		if GeneralConfig.MetaDataResolver == nil {
   155  			GeneralConfig.MetaDataResolver = GetAllStepMetadata
   156  		}
   157  		metadata, err := config.ResolveMetadata(GeneralConfig.GitHubAccessTokens, GeneralConfig.MetaDataResolver, configOptions.stepMetadata, configOptions.stepName)
   158  		if err != nil {
   159  			return stepConfig, errors.Wrapf(err, "failed to resolve metadata")
   160  		}
   161  
   162  		// prepare output resource directories:
   163  		// this is needed in order to have proper directory permissions in case
   164  		// resources written inside a container image with a different user
   165  		// Remark: This is so far only relevant for Jenkins environments where getConfig is executed
   166  
   167  		prepareOutputEnvironment(metadata.Spec.Outputs.Resources, GeneralConfig.EnvRootPath)
   168  
   169  		envParams := metadata.GetResourceParameters(GeneralConfig.EnvRootPath, "commonPipelineEnvironment")
   170  		reportingEnvParams := config.ReportingParameters.GetResourceParameters(GeneralConfig.EnvRootPath, "commonPipelineEnvironment")
   171  		resourceParams := mergeResourceParameters(envParams, reportingEnvParams)
   172  
   173  		projectConfigFile := getProjectConfigFile(GeneralConfig.CustomConfig)
   174  
   175  		customConfig, err := configOptions.openFile(projectConfigFile, GeneralConfig.GitHubAccessTokens)
   176  		if err != nil {
   177  			if !errors.Is(err, os.ErrNotExist) {
   178  				return stepConfig, errors.Wrapf(err, "config: open configuration file '%v' failed", projectConfigFile)
   179  			}
   180  			customConfig = nil
   181  		}
   182  
   183  		defaultConfig, paramFilter, err := defaultsAndFilters(&metadata, metadata.Metadata.Name)
   184  		if err != nil {
   185  			return stepConfig, errors.Wrap(err, "defaults: retrieving step defaults failed")
   186  		}
   187  
   188  		for _, f := range GeneralConfig.DefaultConfig {
   189  			fc, err := configOptions.openFile(f, GeneralConfig.GitHubAccessTokens)
   190  			// only create error for non-default values
   191  			if err != nil && f != ".pipeline/defaults.yaml" {
   192  				return stepConfig, errors.Wrapf(err, "config: getting defaults failed: '%v'", f)
   193  			}
   194  			if err == nil {
   195  				defaultConfig = append(defaultConfig, fc)
   196  			}
   197  		}
   198  
   199  		var flags map[string]interface{}
   200  
   201  		if configOptions.contextConfig {
   202  			metadata.Spec.Inputs.Parameters = []config.StepParameters{}
   203  		}
   204  
   205  		stepConfig, err = myConfig.GetStepConfig(flags, GeneralConfig.ParametersJSON, customConfig, defaultConfig, GeneralConfig.IgnoreCustomDefaults, paramFilter, metadata, resourceParams, GeneralConfig.StageName, metadata.Metadata.Name)
   206  		if err != nil {
   207  			return stepConfig, errors.Wrap(err, "getting step config failed")
   208  		}
   209  
   210  		// apply context conditions if context configuration is requested
   211  		if configOptions.contextConfig {
   212  			applyContextConditions(metadata, &stepConfig)
   213  		}
   214  	}
   215  	return stepConfig, nil
   216  }
   217  
   218  func generateConfig(utils getConfigUtils) error {
   219  
   220  	stepConfig, err := getConfig()
   221  	if err != nil {
   222  		return err
   223  	}
   224  
   225  	myConfigJSON, _ := config.GetJSON(stepConfig.Config)
   226  
   227  	if len(configOptions.outputFile) > 0 {
   228  		err := utils.FileWrite(configOptions.outputFile, []byte(myConfigJSON), 0666)
   229  		if err != nil {
   230  			return fmt.Errorf("failed to write output file %v: %w", configOptions.outputFile, err)
   231  		}
   232  		return nil
   233  	}
   234  	fmt.Println(myConfigJSON)
   235  
   236  	return nil
   237  }
   238  
   239  func addConfigFlags(cmd *cobra.Command) {
   240  
   241  	// ToDo: support more output options, like https://kubernetes.io/docs/reference/kubectl/overview/#formatting-output
   242  	cmd.Flags().StringVar(&configOptions.output, "output", "json", "Defines the output format")
   243  	cmd.Flags().StringVar(&configOptions.outputFile, "outputFile", "", "Defines a file path. f set, the output will be written to the defines file")
   244  
   245  	cmd.Flags().StringVar(&configOptions.parametersJSON, "parametersJSON", os.Getenv("PIPER_parametersJSON"), "Parameters to be considered in JSON format")
   246  	cmd.Flags().BoolVar(&configOptions.stageConfig, "stageConfig", false, "Defines if step stage configuration should be loaded and no step-specific config")
   247  	cmd.Flags().StringArrayVar(&configOptions.stageConfigAcceptedParameters, "stageConfigAcceptedParams", []string{}, "Defines the parameters used for filtering stage/general configuration when accessing stage config")
   248  	cmd.Flags().StringVar(&configOptions.stepMetadata, "stepMetadata", "", "Step metadata, passed as path to yaml")
   249  	cmd.Flags().StringVar(&configOptions.stepName, "stepName", "", "Step name, used to get step metadata if yaml path is not set")
   250  	cmd.Flags().BoolVar(&configOptions.contextConfig, "contextConfig", false, "Defines if step context configuration should be loaded instead of step config")
   251  
   252  }
   253  
   254  func defaultsAndFilters(metadata *config.StepData, stepName string) ([]io.ReadCloser, config.StepFilters, error) {
   255  	if configOptions.contextConfig {
   256  		defaults, err := metadata.GetContextDefaults(stepName)
   257  		if err != nil {
   258  			return nil, config.StepFilters{}, errors.Wrap(err, "metadata: getting context defaults failed")
   259  		}
   260  		return []io.ReadCloser{defaults}, metadata.GetContextParameterFilters(), nil
   261  	}
   262  	// ToDo: retrieve default values from metadata
   263  	return []io.ReadCloser{}, metadata.GetParameterFilters(), nil
   264  }
   265  
   266  func applyContextConditions(metadata config.StepData, stepConfig *config.StepConfig) {
   267  	// consider conditions for context configuration
   268  
   269  	// containers
   270  	config.ApplyContainerConditions(metadata.Spec.Containers, stepConfig)
   271  
   272  	// sidecars
   273  	config.ApplyContainerConditions(metadata.Spec.Sidecars, stepConfig)
   274  
   275  	// ToDo: remove all unnecessary sub maps?
   276  	// e.g. extract delete() from applyContainerConditions - loop over all stepConfig.Config[param.Value] and remove ...
   277  }
   278  
   279  func prepareOutputEnvironment(outputResources []config.StepResources, envRootPath string) {
   280  	for _, oResource := range outputResources {
   281  		for _, oParam := range oResource.Parameters {
   282  			paramPath := path.Join(envRootPath, oResource.Name, fmt.Sprint(oParam["name"]))
   283  			if oParam["fields"] != nil {
   284  				paramFields, ok := oParam["fields"].([]map[string]string)
   285  				if ok && len(paramFields) > 0 {
   286  					paramPath = path.Join(paramPath, paramFields[0]["name"])
   287  				}
   288  			}
   289  			if _, err := os.Stat(filepath.Dir(paramPath)); errors.Is(err, os.ErrNotExist) {
   290  				log.Entry().Debugf("Creating directory: %v", filepath.Dir(paramPath))
   291  				os.MkdirAll(filepath.Dir(paramPath), 0777)
   292  			}
   293  		}
   294  	}
   295  
   296  	// prepare additional output directories known to possibly create permission issues when created from within a container
   297  	// ToDo: evaluate if we can rather call this only in the correct step context (we know the step when calling getConfig!)
   298  	// Could this be part of the container definition in the step.yaml?
   299  	stepOutputDirectories := []string{
   300  		reporting.StepReportDirectory, // standard directory to collect md reports for pipelineCreateScanSummary
   301  		ws.ReportsDirectory,           // standard directory for reports created by whitesourceExecuteScan
   302  	}
   303  
   304  	for _, dir := range stepOutputDirectories {
   305  		if _, err := os.Stat(dir); errors.Is(err, os.ErrNotExist) {
   306  			log.Entry().Debugf("Creating directory: %v", dir)
   307  			os.MkdirAll(dir, 0777)
   308  		}
   309  	}
   310  }