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), &params)
   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  }