github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/cmd/getConfig.go (about)

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