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