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