github.com/jaylevin/jenkins-library@v1.230.4/cmd/piper.go (about)

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