github.com/jaylevin/jenkins-library@v1.230.4/cmd/piper.go (about) 1 package cmd 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "os" 8 "path/filepath" 9 "reflect" 10 "strconv" 11 "strings" 12 13 "github.com/SAP/jenkins-library/pkg/config" 14 "github.com/SAP/jenkins-library/pkg/log" 15 "github.com/SAP/jenkins-library/pkg/orchestrator" 16 "github.com/SAP/jenkins-library/pkg/piperutils" 17 "github.com/pkg/errors" 18 "github.com/spf13/cobra" 19 ) 20 21 // GeneralConfigOptions contains all global configuration options for piper binary 22 type GeneralConfigOptions struct { 23 GitHubAccessTokens map[string]string // map of tokens with url as key in order to maintain url-specific tokens 24 CorrelationID string 25 CustomConfig string 26 GitHubTokens []string // list of entries in form of <server>:<token> to allow token authentication for downloading config / defaults 27 DefaultConfig []string //ordered list of Piper default configurations. Can be filePath or ENV containing JSON in format 'ENV:MY_ENV_VAR' 28 IgnoreCustomDefaults bool 29 ParametersJSON string 30 EnvRootPath string 31 NoTelemetry bool 32 StageName string 33 StepConfigJSON string 34 StepMetadata string //metadata to be considered, can be filePath or ENV containing JSON in format 'ENV:MY_ENV_VAR' 35 StepName string 36 Verbose bool 37 LogFormat string 38 VaultRoleID string 39 VaultRoleSecretID string 40 VaultToken string 41 VaultServerURL string 42 VaultNamespace string 43 VaultPath string 44 HookConfig HookConfiguration 45 MetaDataResolver func() map[string]config.StepData 46 GCPJsonKeyFilePath string 47 GCSFolderPath string 48 GCSBucketId string 49 GCSSubFolder string 50 } 51 52 // HookConfiguration contains the configuration for supported hooks, so far Sentry and Splunk are supported. 53 type HookConfiguration struct { 54 SentryConfig SentryConfiguration `json:"sentry,omitempty"` 55 SplunkConfig SplunkConfiguration `json:"splunk,omitempty"` 56 } 57 58 // SentryConfiguration defines the configuration options for the Sentry logging system 59 type SentryConfiguration struct { 60 Dsn string `json:"dsn,omitempty"` 61 } 62 63 // SplunkConfiguration defines the configuration options for the Splunk logging system 64 type SplunkConfiguration struct { 65 Dsn string `json:"dsn,omitempty"` 66 Token string `json:"token,omitempty"` 67 Index string `json:"index,omitempty"` 68 SendLogs bool `json:"sendLogs"` 69 } 70 71 var rootCmd = &cobra.Command{ 72 Use: "piper", 73 Short: "Executes CI/CD steps from project 'Piper' ", 74 Long: ` 75 This project 'Piper' binary provides a CI/CD step library. 76 It contains many steps which can be used within CI/CD systems as well as directly on e.g. a developer's machine. 77 `, 78 } 79 80 // GeneralConfig contains global configuration flags for piper binary 81 var GeneralConfig GeneralConfigOptions 82 83 // Execute is the starting point of the piper command line tool 84 func Execute() { 85 86 rootCmd.AddCommand(ArtifactPrepareVersionCommand()) 87 rootCmd.AddCommand(ConfigCommand()) 88 rootCmd.AddCommand(DefaultsCommand()) 89 rootCmd.AddCommand(ContainerSaveImageCommand()) 90 rootCmd.AddCommand(CommandLineCompletionCommand()) 91 rootCmd.AddCommand(VersionCommand()) 92 rootCmd.AddCommand(DetectExecuteScanCommand()) 93 rootCmd.AddCommand(HadolintExecuteCommand()) 94 rootCmd.AddCommand(KarmaExecuteTestsCommand()) 95 rootCmd.AddCommand(UiVeri5ExecuteTestsCommand()) 96 rootCmd.AddCommand(SonarExecuteScanCommand()) 97 rootCmd.AddCommand(KubernetesDeployCommand()) 98 rootCmd.AddCommand(HelmExecuteCommand()) 99 rootCmd.AddCommand(XsDeployCommand()) 100 rootCmd.AddCommand(GithubCheckBranchProtectionCommand()) 101 rootCmd.AddCommand(GithubCommentIssueCommand()) 102 rootCmd.AddCommand(GithubCreateIssueCommand()) 103 rootCmd.AddCommand(GithubCreatePullRequestCommand()) 104 rootCmd.AddCommand(GithubPublishReleaseCommand()) 105 rootCmd.AddCommand(GithubSetCommitStatusCommand()) 106 rootCmd.AddCommand(GitopsUpdateDeploymentCommand()) 107 rootCmd.AddCommand(CloudFoundryDeleteServiceCommand()) 108 rootCmd.AddCommand(AbapEnvironmentPullGitRepoCommand()) 109 rootCmd.AddCommand(AbapEnvironmentCloneGitRepoCommand()) 110 rootCmd.AddCommand(AbapEnvironmentCheckoutBranchCommand()) 111 rootCmd.AddCommand(AbapEnvironmentCreateTagCommand()) 112 rootCmd.AddCommand(AbapEnvironmentCreateSystemCommand()) 113 rootCmd.AddCommand(CheckmarxExecuteScanCommand()) 114 rootCmd.AddCommand(FortifyExecuteScanCommand()) 115 rootCmd.AddCommand(MtaBuildCommand()) 116 rootCmd.AddCommand(ProtecodeExecuteScanCommand()) 117 rootCmd.AddCommand(MavenExecuteCommand()) 118 rootCmd.AddCommand(CloudFoundryCreateServiceKeyCommand()) 119 rootCmd.AddCommand(MavenBuildCommand()) 120 rootCmd.AddCommand(MavenExecuteIntegrationCommand()) 121 rootCmd.AddCommand(MavenExecuteStaticCodeChecksCommand()) 122 rootCmd.AddCommand(NexusUploadCommand()) 123 rootCmd.AddCommand(AbapEnvironmentPushATCSystemConfigCommand()) 124 rootCmd.AddCommand(AbapEnvironmentRunATCCheckCommand()) 125 rootCmd.AddCommand(NpmExecuteScriptsCommand()) 126 rootCmd.AddCommand(NpmExecuteLintCommand()) 127 rootCmd.AddCommand(GctsCreateRepositoryCommand()) 128 rootCmd.AddCommand(GctsExecuteABAPQualityChecksCommand()) 129 rootCmd.AddCommand(GctsExecuteABAPUnitTestsCommand()) 130 rootCmd.AddCommand(GctsDeployCommand()) 131 rootCmd.AddCommand(MalwareExecuteScanCommand()) 132 rootCmd.AddCommand(CloudFoundryCreateServiceCommand()) 133 rootCmd.AddCommand(CloudFoundryDeployCommand()) 134 rootCmd.AddCommand(GctsRollbackCommand()) 135 rootCmd.AddCommand(WhitesourceExecuteScanCommand()) 136 rootCmd.AddCommand(GctsCloneRepositoryCommand()) 137 rootCmd.AddCommand(JsonApplyPatchCommand()) 138 rootCmd.AddCommand(KanikoExecuteCommand()) 139 rootCmd.AddCommand(CnbBuildCommand()) 140 rootCmd.AddCommand(AbapEnvironmentBuildCommand()) 141 rootCmd.AddCommand(AbapEnvironmentAssemblePackagesCommand()) 142 rootCmd.AddCommand(AbapAddonAssemblyKitCheckCVsCommand()) 143 rootCmd.AddCommand(AbapAddonAssemblyKitCheckPVCommand()) 144 rootCmd.AddCommand(AbapAddonAssemblyKitCreateTargetVectorCommand()) 145 rootCmd.AddCommand(AbapAddonAssemblyKitPublishTargetVectorCommand()) 146 rootCmd.AddCommand(AbapAddonAssemblyKitRegisterPackagesCommand()) 147 rootCmd.AddCommand(AbapAddonAssemblyKitReleasePackagesCommand()) 148 rootCmd.AddCommand(AbapAddonAssemblyKitReserveNextPackagesCommand()) 149 rootCmd.AddCommand(CloudFoundryCreateSpaceCommand()) 150 rootCmd.AddCommand(CloudFoundryDeleteSpaceCommand()) 151 rootCmd.AddCommand(VaultRotateSecretIdCommand()) 152 rootCmd.AddCommand(IsChangeInDevelopmentCommand()) 153 rootCmd.AddCommand(TransportRequestUploadCTSCommand()) 154 rootCmd.AddCommand(TransportRequestUploadRFCCommand()) 155 rootCmd.AddCommand(NewmanExecuteCommand()) 156 rootCmd.AddCommand(IntegrationArtifactDeployCommand()) 157 rootCmd.AddCommand(TransportRequestUploadSOLMANCommand()) 158 rootCmd.AddCommand(IntegrationArtifactUpdateConfigurationCommand()) 159 rootCmd.AddCommand(IntegrationArtifactGetMplStatusCommand()) 160 rootCmd.AddCommand(IntegrationArtifactGetServiceEndpointCommand()) 161 rootCmd.AddCommand(IntegrationArtifactDownloadCommand()) 162 rootCmd.AddCommand(AbapEnvironmentAssembleConfirmCommand()) 163 rootCmd.AddCommand(IntegrationArtifactUploadCommand()) 164 rootCmd.AddCommand(IntegrationArtifactTriggerIntegrationTestCommand()) 165 rootCmd.AddCommand(IntegrationArtifactUnDeployCommand()) 166 rootCmd.AddCommand(IntegrationArtifactResourceCommand()) 167 rootCmd.AddCommand(TerraformExecuteCommand()) 168 rootCmd.AddCommand(ContainerExecuteStructureTestsCommand()) 169 rootCmd.AddCommand(GaugeExecuteTestsCommand()) 170 rootCmd.AddCommand(BatsExecuteTestsCommand()) 171 rootCmd.AddCommand(PipelineCreateScanSummaryCommand()) 172 rootCmd.AddCommand(TransportRequestDocIDFromGitCommand()) 173 rootCmd.AddCommand(TransportRequestReqIDFromGitCommand()) 174 rootCmd.AddCommand(WritePipelineEnv()) 175 rootCmd.AddCommand(ReadPipelineEnv()) 176 rootCmd.AddCommand(InfluxWriteDataCommand()) 177 rootCmd.AddCommand(AbapEnvironmentRunAUnitTestCommand()) 178 rootCmd.AddCommand(CheckStepActiveCommand()) 179 rootCmd.AddCommand(GolangBuildCommand()) 180 rootCmd.AddCommand(ShellExecuteCommand()) 181 rootCmd.AddCommand(ApiProxyDownloadCommand()) 182 rootCmd.AddCommand(ApiKeyValueMapDownloadCommand()) 183 rootCmd.AddCommand(ApiProviderDownloadCommand()) 184 rootCmd.AddCommand(ApiProxyUploadCommand()) 185 rootCmd.AddCommand(GradleExecuteBuildCommand()) 186 rootCmd.AddCommand(ApiKeyValueMapUploadCommand()) 187 rootCmd.AddCommand(PythonBuildCommand()) 188 rootCmd.AddCommand(AwsS3UploadCommand()) 189 190 addRootFlags(rootCmd) 191 192 if err := rootCmd.Execute(); err != nil { 193 log.SetErrorCategory(log.ErrorConfiguration) 194 log.Entry().WithError(err).Fatal("configuration error") 195 } 196 } 197 198 func addRootFlags(rootCmd *cobra.Command) { 199 var provider orchestrator.OrchestratorSpecificConfigProviding 200 var err error 201 202 provider, err = orchestrator.NewOrchestratorSpecificConfigProvider() 203 if err != nil { 204 log.Entry().Error(err) 205 provider = &orchestrator.UnknownOrchestratorConfigProvider{} 206 } 207 208 rootCmd.PersistentFlags().StringVar(&GeneralConfig.CorrelationID, "correlationID", provider.GetBuildURL(), "ID for unique identification of a pipeline run") 209 rootCmd.PersistentFlags().StringVar(&GeneralConfig.CustomConfig, "customConfig", ".pipeline/config.yml", "Path to the pipeline configuration file") 210 rootCmd.PersistentFlags().StringSliceVar(&GeneralConfig.GitHubTokens, "gitHubTokens", AccessTokensFromEnvJSON(os.Getenv("PIPER_gitHubTokens")), "List of entries in form of <hostname>:<token> to allow GitHub token authentication for downloading config / defaults") 211 rootCmd.PersistentFlags().StringSliceVar(&GeneralConfig.DefaultConfig, "defaultConfig", []string{".pipeline/defaults.yaml"}, "Default configurations, passed as path to yaml file") 212 rootCmd.PersistentFlags().BoolVar(&GeneralConfig.IgnoreCustomDefaults, "ignoreCustomDefaults", false, "Disables evaluation of the parameter 'customDefaults' in the pipeline configuration file") 213 rootCmd.PersistentFlags().StringVar(&GeneralConfig.ParametersJSON, "parametersJSON", os.Getenv("PIPER_parametersJSON"), "Parameters to be considered in JSON format") 214 rootCmd.PersistentFlags().StringVar(&GeneralConfig.EnvRootPath, "envRootPath", ".pipeline", "Root path to Piper pipeline shared environments") 215 rootCmd.PersistentFlags().StringVar(&GeneralConfig.StageName, "stageName", "", "Name of the stage for which configuration should be included") 216 rootCmd.PersistentFlags().StringVar(&GeneralConfig.StepConfigJSON, "stepConfigJSON", os.Getenv("PIPER_stepConfigJSON"), "Step configuration in JSON format") 217 rootCmd.PersistentFlags().BoolVar(&GeneralConfig.NoTelemetry, "noTelemetry", false, "Disables telemetry reporting") 218 rootCmd.PersistentFlags().BoolVarP(&GeneralConfig.Verbose, "verbose", "v", false, "verbose output") 219 rootCmd.PersistentFlags().StringVar(&GeneralConfig.LogFormat, "logFormat", "default", "Log format to use. Options: default, timestamp, plain, full.") 220 rootCmd.PersistentFlags().StringVar(&GeneralConfig.VaultServerURL, "vaultServerUrl", "", "The Vault server which should be used to fetch credentials") 221 rootCmd.PersistentFlags().StringVar(&GeneralConfig.VaultNamespace, "vaultNamespace", "", "The Vault namespace which should be used to fetch credentials") 222 rootCmd.PersistentFlags().StringVar(&GeneralConfig.VaultPath, "vaultPath", "", "The path which should be used to fetch credentials") 223 rootCmd.PersistentFlags().StringVar(&GeneralConfig.GCPJsonKeyFilePath, "gcpJsonKeyFilePath", "", "File path to Google Cloud Platform JSON key file") 224 rootCmd.PersistentFlags().StringVar(&GeneralConfig.GCSFolderPath, "gcsFolderPath", "", "GCS folder path. One of the components of GCS target folder") 225 rootCmd.PersistentFlags().StringVar(&GeneralConfig.GCSBucketId, "gcsBucketId", "", "Bucket name for Google Cloud Storage") 226 rootCmd.PersistentFlags().StringVar(&GeneralConfig.GCSSubFolder, "gcsSubFolder", "", "Used to logically separate results of the same step result type") 227 228 } 229 230 // ResolveAccessTokens reads a list of tokens in format host:token passed via command line 231 // and transfers this into a map as a more consumable format. 232 func ResolveAccessTokens(tokenList []string) map[string]string { 233 tokenMap := map[string]string{} 234 for _, tokenEntry := range tokenList { 235 log.Entry().Debugf("processing token %v", tokenEntry) 236 parts := strings.Split(tokenEntry, ":") 237 if len(parts) != 2 { 238 log.Entry().Warningf("wrong format for access token %v", tokenEntry) 239 } else { 240 tokenMap[parts[0]] = parts[1] 241 } 242 } 243 return tokenMap 244 } 245 246 // AccessTokensFromEnvJSON resolves access tokens when passed as JSON in an environment variable 247 func AccessTokensFromEnvJSON(env string) []string { 248 accessTokens := []string{} 249 if len(env) == 0 { 250 return accessTokens 251 } 252 err := json.Unmarshal([]byte(env), &accessTokens) 253 if err != nil { 254 log.Entry().Infof("Token json '%v' has wrong format.", env) 255 } 256 return accessTokens 257 } 258 259 // initStageName initializes GeneralConfig.StageName from either GeneralConfig.ParametersJSON 260 // or the environment variable (orchestrator specific), unless it has been provided as command line option. 261 // Log output needs to be suppressed via outputToLog by the getConfig step. 262 func initStageName(outputToLog bool) { 263 var stageNameSource string 264 if outputToLog { 265 defer func() { 266 log.Entry().Infof("Using stageName '%s' from %s", GeneralConfig.StageName, stageNameSource) 267 }() 268 } 269 270 if GeneralConfig.StageName != "" { 271 // Means it was given as command line argument and has the highest precedence 272 stageNameSource = "command line arguments" 273 return 274 } 275 276 // Use stageName from ENV as fall-back, for when extracting it from parametersJSON fails below 277 provider, err := orchestrator.NewOrchestratorSpecificConfigProvider() 278 if err != nil { 279 log.Entry().WithError(err).Warning("Cannot infer stage name from CI environment") 280 } else { 281 stageNameSource = "env variable" 282 GeneralConfig.StageName = provider.GetStageName() 283 } 284 285 if len(GeneralConfig.ParametersJSON) == 0 { 286 return 287 } 288 289 var params map[string]interface{} 290 err = json.Unmarshal([]byte(GeneralConfig.ParametersJSON), ¶ms) 291 if err != nil { 292 if outputToLog { 293 log.Entry().Infof("Failed to extract 'stageName' from parametersJSON: %v", err) 294 } 295 return 296 } 297 298 stageName, hasKey := params["stageName"] 299 if !hasKey { 300 return 301 } 302 303 if stageNameString, ok := stageName.(string); ok && stageNameString != "" { 304 stageNameSource = "parametersJSON" 305 GeneralConfig.StageName = stageNameString 306 } 307 } 308 309 // PrepareConfig reads step configuration from various sources and merges it (defaults, config file, flags, ...) 310 func PrepareConfig(cmd *cobra.Command, metadata *config.StepData, stepName string, options interface{}, openFile func(s string, t map[string]string) (io.ReadCloser, error)) error { 311 312 log.SetFormatter(GeneralConfig.LogFormat) 313 314 initStageName(true) 315 316 filters := metadata.GetParameterFilters() 317 318 // add telemetry parameter "collectTelemetryData" to ALL, GENERAL and PARAMETER filters 319 filters.All = append(filters.All, "collectTelemetryData") 320 filters.General = append(filters.General, "collectTelemetryData") 321 filters.Parameters = append(filters.Parameters, "collectTelemetryData") 322 323 envParams := metadata.GetResourceParameters(GeneralConfig.EnvRootPath, "commonPipelineEnvironment") 324 reportingEnvParams := config.ReportingParameters.GetResourceParameters(GeneralConfig.EnvRootPath, "commonPipelineEnvironment") 325 resourceParams := mergeResourceParameters(envParams, reportingEnvParams) 326 327 flagValues := config.AvailableFlagValues(cmd, &filters) 328 329 var myConfig config.Config 330 var stepConfig config.StepConfig 331 332 // add vault credentials so that configuration can be fetched from vault 333 if GeneralConfig.VaultRoleID == "" { 334 GeneralConfig.VaultRoleID = os.Getenv("PIPER_vaultAppRoleID") 335 } 336 if GeneralConfig.VaultRoleSecretID == "" { 337 GeneralConfig.VaultRoleSecretID = os.Getenv("PIPER_vaultAppRoleSecretID") 338 } 339 if GeneralConfig.VaultToken == "" { 340 GeneralConfig.VaultToken = os.Getenv("PIPER_vaultToken") 341 } 342 myConfig.SetVaultCredentials(GeneralConfig.VaultRoleID, GeneralConfig.VaultRoleSecretID, GeneralConfig.VaultToken) 343 344 if len(GeneralConfig.StepConfigJSON) != 0 { 345 // ignore config & defaults in favor of passed stepConfigJSON 346 stepConfig = config.GetStepConfigWithJSON(flagValues, GeneralConfig.StepConfigJSON, filters) 347 log.Entry().Infof("Project config: passed via JSON") 348 log.Entry().Infof("Project defaults: passed via JSON") 349 } else { 350 // use config & defaults 351 var customConfig io.ReadCloser 352 var err error 353 //accept that config file and defaults cannot be loaded since both are not mandatory here 354 { 355 projectConfigFile := getProjectConfigFile(GeneralConfig.CustomConfig) 356 if exists, err := piperutils.FileExists(projectConfigFile); exists { 357 log.Entry().Infof("Project config: '%s'", projectConfigFile) 358 if customConfig, err = openFile(projectConfigFile, GeneralConfig.GitHubAccessTokens); err != nil { 359 return errors.Wrapf(err, "Cannot read '%s'", projectConfigFile) 360 } 361 } else { 362 log.Entry().Infof("Project config: NONE ('%s' does not exist)", projectConfigFile) 363 customConfig = nil 364 } 365 } 366 var defaultConfig []io.ReadCloser 367 if len(GeneralConfig.DefaultConfig) == 0 { 368 log.Entry().Info("Project defaults: NONE") 369 } 370 for _, projectDefaultFile := range GeneralConfig.DefaultConfig { 371 fc, err := openFile(projectDefaultFile, GeneralConfig.GitHubAccessTokens) 372 // only create error for non-default values 373 if err != nil { 374 if projectDefaultFile != ".pipeline/defaults.yaml" { 375 log.Entry().Infof("Project defaults: '%s'", projectDefaultFile) 376 return errors.Wrapf(err, "Cannot read '%s'", projectDefaultFile) 377 } 378 } else { 379 log.Entry().Infof("Project defaults: '%s'", projectDefaultFile) 380 defaultConfig = append(defaultConfig, fc) 381 } 382 } 383 stepConfig, err = myConfig.GetStepConfig(flagValues, GeneralConfig.ParametersJSON, customConfig, defaultConfig, GeneralConfig.IgnoreCustomDefaults, filters, *metadata, resourceParams, GeneralConfig.StageName, stepName) 384 if verbose, ok := stepConfig.Config["verbose"].(bool); ok && verbose { 385 log.SetVerbose(verbose) 386 GeneralConfig.Verbose = verbose 387 } else if !ok && stepConfig.Config["verbose"] != nil { 388 log.Entry().Warnf("invalid value for parameter verbose: '%v'", stepConfig.Config["verbose"]) 389 } 390 if err != nil { 391 return errors.Wrap(err, "retrieving step configuration failed") 392 } 393 } 394 395 if fmt.Sprintf("%v", stepConfig.Config["collectTelemetryData"]) == "false" { 396 GeneralConfig.NoTelemetry = true 397 } 398 399 stepConfig.Config = checkTypes(stepConfig.Config, options) 400 confJSON, _ := json.Marshal(stepConfig.Config) 401 _ = json.Unmarshal(confJSON, &options) 402 403 config.MarkFlagsWithValue(cmd, stepConfig) 404 405 retrieveHookConfig(stepConfig.HookConfig, &GeneralConfig.HookConfig) 406 407 if GeneralConfig.GCPJsonKeyFilePath == "" { 408 GeneralConfig.GCPJsonKeyFilePath, _ = stepConfig.Config["gcpJsonKeyFilePath"].(string) 409 } 410 if GeneralConfig.GCSFolderPath == "" { 411 GeneralConfig.GCSFolderPath, _ = stepConfig.Config["gcsFolderPath"].(string) 412 } 413 if GeneralConfig.GCSBucketId == "" { 414 GeneralConfig.GCSBucketId, _ = stepConfig.Config["gcsBucketId"].(string) 415 } 416 if GeneralConfig.GCSSubFolder == "" { 417 GeneralConfig.GCSSubFolder, _ = stepConfig.Config["gcsSubFolder"].(string) 418 } 419 return nil 420 } 421 422 func retrieveHookConfig(source map[string]interface{}, target *HookConfiguration) { 423 if source != nil { 424 log.Entry().Info("Retrieving hook configuration") 425 b, err := json.Marshal(source) 426 if err != nil { 427 log.Entry().Warningf("Failed to marshal source hook configuration: %v", err) 428 } 429 err = json.Unmarshal(b, target) 430 if err != nil { 431 log.Entry().Warningf("Failed to retrieve hook configuration: %v", err) 432 } 433 } 434 } 435 436 var errIncompatibleTypes = fmt.Errorf("incompatible types") 437 438 func checkTypes(config map[string]interface{}, options interface{}) map[string]interface{} { 439 optionsType := getStepOptionsStructType(options) 440 441 for paramName := range config { 442 optionsField := findStructFieldByJSONTag(paramName, optionsType) 443 if optionsField == nil { 444 continue 445 } 446 447 if config[paramName] == nil { 448 // There is a key, but no value. This can result from merging values from the CPE. 449 continue 450 } 451 452 paramValueType := reflect.ValueOf(config[paramName]) 453 if optionsField.Type.Kind() == paramValueType.Kind() { 454 // Types already match, nothing to do 455 continue 456 } 457 458 var typeError error = nil 459 460 switch paramValueType.Kind() { 461 case reflect.String: 462 typeError = convertValueFromString(config, optionsField, paramName, paramValueType.String()) 463 case reflect.Float32, reflect.Float64: 464 typeError = convertValueFromFloat(config, optionsField, paramName, paramValueType.Float()) 465 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 466 typeError = convertValueFromInt(config, optionsField, paramName, paramValueType.Int()) 467 default: 468 log.Entry().Warnf("Config value for '%s' is of unexpected type %s, expected %s. "+ 469 "The value may be ignored as a result. To avoid any risk, specify this value with explicit type.", 470 paramName, paramValueType.Kind(), optionsField.Type.Kind()) 471 } 472 473 if typeError != nil { 474 typeError = fmt.Errorf("config value for '%s' is of unexpected type %s, expected %s: %w", 475 paramName, paramValueType.Kind(), optionsField.Type.Kind(), typeError) 476 log.SetErrorCategory(log.ErrorConfiguration) 477 log.Entry().WithError(typeError).Fatal("type error in configuration") 478 } 479 } 480 return config 481 } 482 483 func convertValueFromString(config map[string]interface{}, optionsField *reflect.StructField, paramName, paramValue string) error { 484 switch optionsField.Type.Kind() { 485 case reflect.Slice, reflect.Array: 486 // Could do automatic conversion for those types in theory, 487 // but that might obscure what really happens in error cases. 488 return fmt.Errorf("expected type to be a list (or slice, or array) but got string") 489 case reflect.Bool: 490 // Sensible to convert strings "true"/"false" to respective boolean values as it is 491 // common practice to write booleans as string in yaml files. 492 paramValue = strings.ToLower(paramValue) 493 if paramValue == "true" { 494 config[paramName] = true 495 return nil 496 } else if paramValue == "false" { 497 config[paramName] = false 498 return nil 499 } 500 } 501 502 return errIncompatibleTypes 503 } 504 505 func convertValueFromFloat(config map[string]interface{}, optionsField *reflect.StructField, paramName string, paramValue float64) error { 506 switch optionsField.Type.Kind() { 507 case reflect.String: 508 val := strconv.FormatFloat(paramValue, 'f', -1, 64) 509 // if Sprinted value and val are equal, we can be pretty sure that the result fits 510 // for very large numbers for example an exponential format is printed 511 if val == fmt.Sprint(paramValue) { 512 config[paramName] = val 513 return nil 514 } 515 // allow float numbers containing a decimal separator 516 if strings.Contains(val, ".") { 517 config[paramName] = val 518 return nil 519 } 520 // if now no decimal separator is available we cannot be sure that the result is correct: 521 // long numbers like e.g. 73554900100200011600 will not be represented correctly after reading the yaml 522 // thus we cannot assume that the string is correct. 523 // short numbers will be handled as int anyway 524 return errIncompatibleTypes 525 case reflect.Float32: 526 config[paramName] = float32(paramValue) 527 return nil 528 case reflect.Float64: 529 config[paramName] = paramValue 530 return nil 531 case reflect.Int: 532 // Treat as type-mismatch only in case the conversion would be lossy. 533 // In that case, the json.Unmarshall() would indeed just drop it, so we want to fail. 534 if float64(int(paramValue)) == paramValue { 535 config[paramName] = int(paramValue) 536 return nil 537 } 538 } 539 540 return errIncompatibleTypes 541 } 542 543 func convertValueFromInt(config map[string]interface{}, optionsField *reflect.StructField, paramName string, paramValue int64) error { 544 switch optionsField.Type.Kind() { 545 case reflect.String: 546 config[paramName] = strconv.FormatInt(paramValue, 10) 547 return nil 548 case reflect.Float32: 549 config[paramName] = float32(paramValue) 550 return nil 551 case reflect.Float64: 552 config[paramName] = float64(paramValue) 553 return nil 554 } 555 556 return errIncompatibleTypes 557 } 558 559 func findStructFieldByJSONTag(tagName string, optionsType reflect.Type) *reflect.StructField { 560 for i := 0; i < optionsType.NumField(); i++ { 561 field := optionsType.Field(i) 562 tag := field.Tag.Get("json") 563 if tagName == tag || tagName+",omitempty" == tag { 564 return &field 565 } 566 } 567 return nil 568 } 569 570 func getStepOptionsStructType(stepOptions interface{}) reflect.Type { 571 typedOptions := reflect.ValueOf(stepOptions) 572 if typedOptions.Kind() == reflect.Ptr { 573 typedOptions = typedOptions.Elem() 574 } 575 return typedOptions.Type() 576 } 577 578 func getProjectConfigFile(name string) string { 579 580 var altName string 581 if ext := filepath.Ext(name); ext == ".yml" { 582 altName = fmt.Sprintf("%v.yaml", strings.TrimSuffix(name, ext)) 583 } else if ext == "yaml" { 584 altName = fmt.Sprintf("%v.yml", strings.TrimSuffix(name, ext)) 585 } 586 587 fileExists, _ := piperutils.FileExists(name) 588 altExists, _ := piperutils.FileExists(altName) 589 590 // configured filename will always take precedence, even if not existing 591 if !fileExists && altExists { 592 return altName 593 } 594 return name 595 } 596 597 func mergeResourceParameters(resParams ...map[string]interface{}) map[string]interface{} { 598 result := make(map[string]interface{}) 599 for _, m := range resParams { 600 for k, v := range m { 601 result[k] = v 602 } 603 } 604 return result 605 }