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 }