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