1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2024-12-12 10:55:20 +02:00
sap-jenkins-library/cmd/piper.go
Artem Bannikov 4b257377ec
[TMS] Reimplement tmsUpload step in Go (#3399)
* Initially generated tmsUpload<...> files

* First provisioning of parameters supported by tmsUpload step

* Refer to Go step from tmsUpload.groovy

* Initial client implementation

* Reverting line delimiters in tmsUpoad.groovy back to Unix ones

* Temporarily remove when-condition for Release stage

* Define useGoStep parameter in tmsUpload.groovy

* Unstash buildResult if useGoStep is true

* No unstashing and empty credentials, when using go step

* Register TmsUploadCommand in piper.go

* Cleanup groovy-related changes - they will be temporarily implemented in a different repo

* Make getting OAuth token success

* Look through the code and cleanup it a bit

* Read service key from Jenkins credentials store

* Provide initial set of unit tests for methods in /pkg/tms/tms.go file

* Minor improvements on logging response on http call error

* Check, if positive HTTP status code is as expected

* Cleanup tms.yaml file, provide additional unit test for tms.go

* Provide unit test for the case, when request body contains spaces

* Specify nodeExtDescriptorMapping parameter as of type map in tms.yaml

* Implement client method for getting nodes

* Write tests for GetNodes method

* Add GetMtaExtDescriptor client method and cover it with unit tests

* Provide first implementation for Update- and UploadMtaExtDescriptor
client methods

* Provide first implementation for Update- and UploadMtaExtDescriptor
client methods

* Provide UploadFile and UploadFileToNode client methods

* Provide tests for Update- and UploadMtaExtDescriptor client methods

* Write tests for FileUpload and FileUploadToNode client methods

* Minor corrections

* Remove some TODO comments

* Rename some of response structures

* Revert change for line delimiters in cmd/piper.go

* Add uploadType string parameter to UploadFile and UploadRequest methods
of uploader mock to reflect the changed Uploader implementation

* Start to implement execution logic in tmsUpload.go

* Changes in tms.yaml file

- remove resources from inputs in tms.yaml and implement mtaPath
parameter settings in the yaml file the same way, as it is done in
cloudFoundryDeploy.yaml
- rename tms.yaml to tmsUpload.yaml, since some generation policy
changed meanwhile

* Rename tms.yaml to tmsUpload.yaml and do go generate

* Use provided proxy on communication with UAA and TMS

* Set proxy even before getting OAuth token

* Further implementation of tmsUpload.go

* Continuation on implementing the tmsUpload.go executor

* Get mtarFilePath and git commitId from commonPipelineEnvironment, if
they are missing in configuration file + minor changes

* Implement a happy path test for tmsUpload logic

* Cover with unit tests main happy and error paths of tmsUpload.go logic

* Extend set of unit tests for tmsUpload.go

* Eliminate some TODOs, extend unit tests for tmsUpload.go

* Delete some TODOs

* Remove a couple of more TODOs from tms_test.go file

* Provide additional unit test for error due unexpected positive http
status code on upload

* Revert back line delimiters in cmd/piper.go

* Comment out file uploading calls in tmsUpload.go

* Run go generate to update generated files

* Convert line delimiters in tmsUpload.yaml to Unix ones, as well as
provide new line character in the end of the file, plus minor fix for
logging in tmsUpload.go file (pipeline complained)

* Correct description of a parameter in tmsUpload.yaml, extend unit tests
to check for trimming a slash in the end of TMS url for client methods
that do upload

* [minor] Add a comment in the test code

* Add stashContent parameter to do unstashing in tmsUpload.groovy, remove
some of the clarified TODOs

* Uncomment uploading file calls in tmsUpload.go, declare buildResult
stash in tmsUpload.yaml

* Remove clarified TODOs from the tmsUpload.go file

* Run go fmt for jenkins-library/pkg/tms

* Do not get explicitly values from common pipeline environment - all
configurations are provided in yaml file

* Remove unused struct from tmsUpload_test.go

* Run go fmt jenkins-library\pkg\tms

* Revise descriptions of parameters provided in tmsUpload.yaml file

* Specify STAGES scope for tmsUpload parameters

* Provide STAGES scope for the tmsUpload parameters, provide default value
for stashContent parameter

* Remove trailing space from tmsUpload.yaml

* Provide unit tests for proxy-related changes in http.go file

* Improve proxy implementation in tmsUpload.go file

* Make tmsServiceKey again a mandatory parameter

* Run go generate command to make the generated files correspond the yaml
state

* Change line delimiters back to Unix ones (were switched while resolving
the conflicts)

* Remove trailing spaces from tmsUpload.yaml

* Minor change in a comment to trigger pipelines with commit

* Improve checks for zero-structs and for empty maps, as well as use
different package to read files in the tests

* Revert line endings in http.go

* Revert comments formatting changes in files that do not belong to the tmsUpload step
2022-08-30 10:16:09 +02:00

612 lines
25 KiB
Go

package cmd
import (
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
"github.com/SAP/jenkins-library/pkg/config"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/orchestrator"
"github.com/SAP/jenkins-library/pkg/piperutils"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
// GeneralConfigOptions contains all global configuration options for piper binary
type GeneralConfigOptions struct {
GitHubAccessTokens map[string]string // map of tokens with url as key in order to maintain url-specific tokens
CorrelationID string
CustomConfig string
GitHubTokens []string // list of entries in form of <server>:<token> to allow token authentication for downloading config / defaults
DefaultConfig []string //ordered list of Piper default configurations. Can be filePath or ENV containing JSON in format 'ENV:MY_ENV_VAR'
IgnoreCustomDefaults bool
ParametersJSON string
EnvRootPath string
NoTelemetry bool
StageName string
StepConfigJSON string
StepMetadata string //metadata to be considered, can be filePath or ENV containing JSON in format 'ENV:MY_ENV_VAR'
StepName string
Verbose bool
LogFormat string
VaultRoleID string
VaultRoleSecretID string
VaultToken string
VaultServerURL string
VaultNamespace string
VaultPath string
HookConfig HookConfiguration
MetaDataResolver func() map[string]config.StepData
GCPJsonKeyFilePath string
GCSFolderPath string
GCSBucketId string
GCSSubFolder string
}
// HookConfiguration contains the configuration for supported hooks, so far Sentry and Splunk are supported.
type HookConfiguration struct {
SentryConfig SentryConfiguration `json:"sentry,omitempty"`
SplunkConfig SplunkConfiguration `json:"splunk,omitempty"`
}
// SentryConfiguration defines the configuration options for the Sentry logging system
type SentryConfiguration struct {
Dsn string `json:"dsn,omitempty"`
}
// SplunkConfiguration defines the configuration options for the Splunk logging system
type SplunkConfiguration struct {
Dsn string `json:"dsn,omitempty"`
Token string `json:"token,omitempty"`
Index string `json:"index,omitempty"`
SendLogs bool `json:"sendLogs"`
}
var rootCmd = &cobra.Command{
Use: "piper",
Short: "Executes CI/CD steps from project 'Piper' ",
Long: `
This project 'Piper' binary provides a CI/CD step library.
It contains many steps which can be used within CI/CD systems as well as directly on e.g. a developer's machine.
`,
}
// GeneralConfig contains global configuration flags for piper binary
var GeneralConfig GeneralConfigOptions
// Execute is the starting point of the piper command line tool
func Execute() {
rootCmd.AddCommand(ArtifactPrepareVersionCommand())
rootCmd.AddCommand(ConfigCommand())
rootCmd.AddCommand(DefaultsCommand())
rootCmd.AddCommand(ContainerSaveImageCommand())
rootCmd.AddCommand(CommandLineCompletionCommand())
rootCmd.AddCommand(VersionCommand())
rootCmd.AddCommand(DetectExecuteScanCommand())
rootCmd.AddCommand(HadolintExecuteCommand())
rootCmd.AddCommand(KarmaExecuteTestsCommand())
rootCmd.AddCommand(UiVeri5ExecuteTestsCommand())
rootCmd.AddCommand(SonarExecuteScanCommand())
rootCmd.AddCommand(KubernetesDeployCommand())
rootCmd.AddCommand(HelmExecuteCommand())
rootCmd.AddCommand(XsDeployCommand())
rootCmd.AddCommand(GithubCheckBranchProtectionCommand())
rootCmd.AddCommand(GithubCommentIssueCommand())
rootCmd.AddCommand(GithubCreateIssueCommand())
rootCmd.AddCommand(GithubCreatePullRequestCommand())
rootCmd.AddCommand(GithubPublishReleaseCommand())
rootCmd.AddCommand(GithubSetCommitStatusCommand())
rootCmd.AddCommand(GitopsUpdateDeploymentCommand())
rootCmd.AddCommand(CloudFoundryDeleteServiceCommand())
rootCmd.AddCommand(AbapEnvironmentPullGitRepoCommand())
rootCmd.AddCommand(AbapEnvironmentCloneGitRepoCommand())
rootCmd.AddCommand(AbapEnvironmentCheckoutBranchCommand())
rootCmd.AddCommand(AbapEnvironmentCreateTagCommand())
rootCmd.AddCommand(AbapEnvironmentCreateSystemCommand())
rootCmd.AddCommand(CheckmarxExecuteScanCommand())
rootCmd.AddCommand(FortifyExecuteScanCommand())
rootCmd.AddCommand(CodeqlExecuteScanCommand())
rootCmd.AddCommand(MtaBuildCommand())
rootCmd.AddCommand(ProtecodeExecuteScanCommand())
rootCmd.AddCommand(MavenExecuteCommand())
rootCmd.AddCommand(CloudFoundryCreateServiceKeyCommand())
rootCmd.AddCommand(MavenBuildCommand())
rootCmd.AddCommand(MavenExecuteIntegrationCommand())
rootCmd.AddCommand(MavenExecuteStaticCodeChecksCommand())
rootCmd.AddCommand(NexusUploadCommand())
rootCmd.AddCommand(AbapEnvironmentPushATCSystemConfigCommand())
rootCmd.AddCommand(AbapEnvironmentRunATCCheckCommand())
rootCmd.AddCommand(NpmExecuteScriptsCommand())
rootCmd.AddCommand(NpmExecuteLintCommand())
rootCmd.AddCommand(GctsCreateRepositoryCommand())
rootCmd.AddCommand(GctsExecuteABAPQualityChecksCommand())
rootCmd.AddCommand(GctsExecuteABAPUnitTestsCommand())
rootCmd.AddCommand(GctsDeployCommand())
rootCmd.AddCommand(MalwareExecuteScanCommand())
rootCmd.AddCommand(CloudFoundryCreateServiceCommand())
rootCmd.AddCommand(CloudFoundryDeployCommand())
rootCmd.AddCommand(GctsRollbackCommand())
rootCmd.AddCommand(WhitesourceExecuteScanCommand())
rootCmd.AddCommand(GctsCloneRepositoryCommand())
rootCmd.AddCommand(JsonApplyPatchCommand())
rootCmd.AddCommand(KanikoExecuteCommand())
rootCmd.AddCommand(CnbBuildCommand())
rootCmd.AddCommand(AbapEnvironmentBuildCommand())
rootCmd.AddCommand(AbapEnvironmentAssemblePackagesCommand())
rootCmd.AddCommand(AbapAddonAssemblyKitCheckCVsCommand())
rootCmd.AddCommand(AbapAddonAssemblyKitCheckPVCommand())
rootCmd.AddCommand(AbapAddonAssemblyKitCreateTargetVectorCommand())
rootCmd.AddCommand(AbapAddonAssemblyKitPublishTargetVectorCommand())
rootCmd.AddCommand(AbapAddonAssemblyKitRegisterPackagesCommand())
rootCmd.AddCommand(AbapAddonAssemblyKitReleasePackagesCommand())
rootCmd.AddCommand(AbapAddonAssemblyKitReserveNextPackagesCommand())
rootCmd.AddCommand(CloudFoundryCreateSpaceCommand())
rootCmd.AddCommand(CloudFoundryDeleteSpaceCommand())
rootCmd.AddCommand(VaultRotateSecretIdCommand())
rootCmd.AddCommand(IsChangeInDevelopmentCommand())
rootCmd.AddCommand(TransportRequestUploadCTSCommand())
rootCmd.AddCommand(TransportRequestUploadRFCCommand())
rootCmd.AddCommand(NewmanExecuteCommand())
rootCmd.AddCommand(IntegrationArtifactDeployCommand())
rootCmd.AddCommand(TransportRequestUploadSOLMANCommand())
rootCmd.AddCommand(IntegrationArtifactUpdateConfigurationCommand())
rootCmd.AddCommand(IntegrationArtifactGetMplStatusCommand())
rootCmd.AddCommand(IntegrationArtifactGetServiceEndpointCommand())
rootCmd.AddCommand(IntegrationArtifactDownloadCommand())
rootCmd.AddCommand(AbapEnvironmentAssembleConfirmCommand())
rootCmd.AddCommand(IntegrationArtifactUploadCommand())
rootCmd.AddCommand(IntegrationArtifactTriggerIntegrationTestCommand())
rootCmd.AddCommand(IntegrationArtifactUnDeployCommand())
rootCmd.AddCommand(IntegrationArtifactResourceCommand())
rootCmd.AddCommand(TerraformExecuteCommand())
rootCmd.AddCommand(ContainerExecuteStructureTestsCommand())
rootCmd.AddCommand(GaugeExecuteTestsCommand())
rootCmd.AddCommand(BatsExecuteTestsCommand())
rootCmd.AddCommand(PipelineCreateScanSummaryCommand())
rootCmd.AddCommand(TransportRequestDocIDFromGitCommand())
rootCmd.AddCommand(TransportRequestReqIDFromGitCommand())
rootCmd.AddCommand(WritePipelineEnv())
rootCmd.AddCommand(ReadPipelineEnv())
rootCmd.AddCommand(InfluxWriteDataCommand())
rootCmd.AddCommand(AbapEnvironmentRunAUnitTestCommand())
rootCmd.AddCommand(CheckStepActiveCommand())
rootCmd.AddCommand(GolangBuildCommand())
rootCmd.AddCommand(ShellExecuteCommand())
rootCmd.AddCommand(ApiProxyDownloadCommand())
rootCmd.AddCommand(ApiKeyValueMapDownloadCommand())
rootCmd.AddCommand(ApiProviderDownloadCommand())
rootCmd.AddCommand(ApiProxyUploadCommand())
rootCmd.AddCommand(GradleExecuteBuildCommand())
rootCmd.AddCommand(ApiKeyValueMapUploadCommand())
rootCmd.AddCommand(PythonBuildCommand())
rootCmd.AddCommand(AzureBlobUploadCommand())
rootCmd.AddCommand(AwsS3UploadCommand())
rootCmd.AddCommand(ApiProxyListCommand())
rootCmd.AddCommand(AnsSendEventCommand())
rootCmd.AddCommand(ApiProviderListCommand())
rootCmd.AddCommand(TmsUploadCommand())
addRootFlags(rootCmd)
if err := rootCmd.Execute(); err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
log.Entry().WithError(err).Fatal("configuration error")
}
}
func addRootFlags(rootCmd *cobra.Command) {
var provider orchestrator.OrchestratorSpecificConfigProviding
var err error
provider, err = orchestrator.NewOrchestratorSpecificConfigProvider()
if err != nil {
log.Entry().Error(err)
provider = &orchestrator.UnknownOrchestratorConfigProvider{}
}
rootCmd.PersistentFlags().StringVar(&GeneralConfig.CorrelationID, "correlationID", provider.GetBuildURL(), "ID for unique identification of a pipeline run")
rootCmd.PersistentFlags().StringVar(&GeneralConfig.CustomConfig, "customConfig", ".pipeline/config.yml", "Path to the pipeline configuration file")
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")
rootCmd.PersistentFlags().StringSliceVar(&GeneralConfig.DefaultConfig, "defaultConfig", []string{".pipeline/defaults.yaml"}, "Default configurations, passed as path to yaml file")
rootCmd.PersistentFlags().BoolVar(&GeneralConfig.IgnoreCustomDefaults, "ignoreCustomDefaults", false, "Disables evaluation of the parameter 'customDefaults' in the pipeline configuration file")
rootCmd.PersistentFlags().StringVar(&GeneralConfig.ParametersJSON, "parametersJSON", os.Getenv("PIPER_parametersJSON"), "Parameters to be considered in JSON format")
rootCmd.PersistentFlags().StringVar(&GeneralConfig.EnvRootPath, "envRootPath", ".pipeline", "Root path to Piper pipeline shared environments")
rootCmd.PersistentFlags().StringVar(&GeneralConfig.StageName, "stageName", "", "Name of the stage for which configuration should be included")
rootCmd.PersistentFlags().StringVar(&GeneralConfig.StepConfigJSON, "stepConfigJSON", os.Getenv("PIPER_stepConfigJSON"), "Step configuration in JSON format")
rootCmd.PersistentFlags().BoolVar(&GeneralConfig.NoTelemetry, "noTelemetry", false, "Disables telemetry reporting")
rootCmd.PersistentFlags().BoolVarP(&GeneralConfig.Verbose, "verbose", "v", false, "verbose output")
rootCmd.PersistentFlags().StringVar(&GeneralConfig.LogFormat, "logFormat", "default", "Log format to use. Options: default, timestamp, plain, full.")
rootCmd.PersistentFlags().StringVar(&GeneralConfig.VaultServerURL, "vaultServerUrl", "", "The Vault server which should be used to fetch credentials")
rootCmd.PersistentFlags().StringVar(&GeneralConfig.VaultNamespace, "vaultNamespace", "", "The Vault namespace which should be used to fetch credentials")
rootCmd.PersistentFlags().StringVar(&GeneralConfig.VaultPath, "vaultPath", "", "The path which should be used to fetch credentials")
rootCmd.PersistentFlags().StringVar(&GeneralConfig.GCPJsonKeyFilePath, "gcpJsonKeyFilePath", "", "File path to Google Cloud Platform JSON key file")
rootCmd.PersistentFlags().StringVar(&GeneralConfig.GCSFolderPath, "gcsFolderPath", "", "GCS folder path. One of the components of GCS target folder")
rootCmd.PersistentFlags().StringVar(&GeneralConfig.GCSBucketId, "gcsBucketId", "", "Bucket name for Google Cloud Storage")
rootCmd.PersistentFlags().StringVar(&GeneralConfig.GCSSubFolder, "gcsSubFolder", "", "Used to logically separate results of the same step result type")
}
// ResolveAccessTokens reads a list of tokens in format host:token passed via command line
// and transfers this into a map as a more consumable format.
func ResolveAccessTokens(tokenList []string) map[string]string {
tokenMap := map[string]string{}
for _, tokenEntry := range tokenList {
log.Entry().Debugf("processing token %v", tokenEntry)
parts := strings.Split(tokenEntry, ":")
if len(parts) != 2 {
log.Entry().Warningf("wrong format for access token %v", tokenEntry)
} else {
tokenMap[parts[0]] = parts[1]
}
}
return tokenMap
}
// AccessTokensFromEnvJSON resolves access tokens when passed as JSON in an environment variable
func AccessTokensFromEnvJSON(env string) []string {
accessTokens := []string{}
if len(env) == 0 {
return accessTokens
}
err := json.Unmarshal([]byte(env), &accessTokens)
if err != nil {
log.Entry().Infof("Token json '%v' has wrong format.", env)
}
return accessTokens
}
// initStageName initializes GeneralConfig.StageName from either GeneralConfig.ParametersJSON
// or the environment variable (orchestrator specific), unless it has been provided as command line option.
// Log output needs to be suppressed via outputToLog by the getConfig step.
func initStageName(outputToLog bool) {
var stageNameSource string
if outputToLog {
defer func() {
log.Entry().Infof("Using stageName '%s' from %s", GeneralConfig.StageName, stageNameSource)
}()
}
if GeneralConfig.StageName != "" {
// Means it was given as command line argument and has the highest precedence
stageNameSource = "command line arguments"
return
}
// Use stageName from ENV as fall-back, for when extracting it from parametersJSON fails below
provider, err := orchestrator.NewOrchestratorSpecificConfigProvider()
if err != nil {
log.Entry().WithError(err).Warning("Cannot infer stage name from CI environment")
} else {
stageNameSource = "env variable"
GeneralConfig.StageName = provider.GetStageName()
}
if len(GeneralConfig.ParametersJSON) == 0 {
return
}
var params map[string]interface{}
err = json.Unmarshal([]byte(GeneralConfig.ParametersJSON), &params)
if err != nil {
if outputToLog {
log.Entry().Infof("Failed to extract 'stageName' from parametersJSON: %v", err)
}
return
}
stageName, hasKey := params["stageName"]
if !hasKey {
return
}
if stageNameString, ok := stageName.(string); ok && stageNameString != "" {
stageNameSource = "parametersJSON"
GeneralConfig.StageName = stageNameString
}
}
// PrepareConfig reads step configuration from various sources and merges it (defaults, config file, flags, ...)
func PrepareConfig(cmd *cobra.Command, metadata *config.StepData, stepName string, options interface{}, openFile func(s string, t map[string]string) (io.ReadCloser, error)) error {
log.SetFormatter(GeneralConfig.LogFormat)
initStageName(true)
filters := metadata.GetParameterFilters()
// add telemetry parameter "collectTelemetryData" to ALL, GENERAL and PARAMETER filters
filters.All = append(filters.All, "collectTelemetryData")
filters.General = append(filters.General, "collectTelemetryData")
filters.Parameters = append(filters.Parameters, "collectTelemetryData")
envParams := metadata.GetResourceParameters(GeneralConfig.EnvRootPath, "commonPipelineEnvironment")
reportingEnvParams := config.ReportingParameters.GetResourceParameters(GeneralConfig.EnvRootPath, "commonPipelineEnvironment")
resourceParams := mergeResourceParameters(envParams, reportingEnvParams)
flagValues := config.AvailableFlagValues(cmd, &filters)
var myConfig config.Config
var stepConfig config.StepConfig
// add vault credentials so that configuration can be fetched from vault
if GeneralConfig.VaultRoleID == "" {
GeneralConfig.VaultRoleID = os.Getenv("PIPER_vaultAppRoleID")
}
if GeneralConfig.VaultRoleSecretID == "" {
GeneralConfig.VaultRoleSecretID = os.Getenv("PIPER_vaultAppRoleSecretID")
}
if GeneralConfig.VaultToken == "" {
GeneralConfig.VaultToken = os.Getenv("PIPER_vaultToken")
}
myConfig.SetVaultCredentials(GeneralConfig.VaultRoleID, GeneralConfig.VaultRoleSecretID, GeneralConfig.VaultToken)
if len(GeneralConfig.StepConfigJSON) != 0 {
// ignore config & defaults in favor of passed stepConfigJSON
stepConfig = config.GetStepConfigWithJSON(flagValues, GeneralConfig.StepConfigJSON, filters)
log.Entry().Infof("Project config: passed via JSON")
log.Entry().Infof("Project defaults: passed via JSON")
} else {
// use config & defaults
var customConfig io.ReadCloser
var err error
//accept that config file and defaults cannot be loaded since both are not mandatory here
{
projectConfigFile := getProjectConfigFile(GeneralConfig.CustomConfig)
if exists, err := piperutils.FileExists(projectConfigFile); exists {
log.Entry().Infof("Project config: '%s'", projectConfigFile)
if customConfig, err = openFile(projectConfigFile, GeneralConfig.GitHubAccessTokens); err != nil {
return errors.Wrapf(err, "Cannot read '%s'", projectConfigFile)
}
} else {
log.Entry().Infof("Project config: NONE ('%s' does not exist)", projectConfigFile)
customConfig = nil
}
}
var defaultConfig []io.ReadCloser
if len(GeneralConfig.DefaultConfig) == 0 {
log.Entry().Info("Project defaults: NONE")
}
for _, projectDefaultFile := range GeneralConfig.DefaultConfig {
fc, err := openFile(projectDefaultFile, GeneralConfig.GitHubAccessTokens)
// only create error for non-default values
if err != nil {
if projectDefaultFile != ".pipeline/defaults.yaml" {
log.Entry().Infof("Project defaults: '%s'", projectDefaultFile)
return errors.Wrapf(err, "Cannot read '%s'", projectDefaultFile)
}
} else {
log.Entry().Infof("Project defaults: '%s'", projectDefaultFile)
defaultConfig = append(defaultConfig, fc)
}
}
stepConfig, err = myConfig.GetStepConfig(flagValues, GeneralConfig.ParametersJSON, customConfig, defaultConfig, GeneralConfig.IgnoreCustomDefaults, filters, *metadata, resourceParams, GeneralConfig.StageName, stepName)
if verbose, ok := stepConfig.Config["verbose"].(bool); ok && verbose {
log.SetVerbose(verbose)
GeneralConfig.Verbose = verbose
} else if !ok && stepConfig.Config["verbose"] != nil {
log.Entry().Warnf("invalid value for parameter verbose: '%v'", stepConfig.Config["verbose"])
}
if err != nil {
return errors.Wrap(err, "retrieving step configuration failed")
}
}
if fmt.Sprintf("%v", stepConfig.Config["collectTelemetryData"]) == "false" {
GeneralConfig.NoTelemetry = true
}
stepConfig.Config = checkTypes(stepConfig.Config, options)
confJSON, _ := json.Marshal(stepConfig.Config)
_ = json.Unmarshal(confJSON, &options)
config.MarkFlagsWithValue(cmd, stepConfig)
retrieveHookConfig(stepConfig.HookConfig, &GeneralConfig.HookConfig)
if GeneralConfig.GCPJsonKeyFilePath == "" {
GeneralConfig.GCPJsonKeyFilePath, _ = stepConfig.Config["gcpJsonKeyFilePath"].(string)
}
if GeneralConfig.GCSFolderPath == "" {
GeneralConfig.GCSFolderPath, _ = stepConfig.Config["gcsFolderPath"].(string)
}
if GeneralConfig.GCSBucketId == "" {
GeneralConfig.GCSBucketId, _ = stepConfig.Config["gcsBucketId"].(string)
}
if GeneralConfig.GCSSubFolder == "" {
GeneralConfig.GCSSubFolder, _ = stepConfig.Config["gcsSubFolder"].(string)
}
return nil
}
func retrieveHookConfig(source map[string]interface{}, target *HookConfiguration) {
if source != nil {
log.Entry().Info("Retrieving hook configuration")
b, err := json.Marshal(source)
if err != nil {
log.Entry().Warningf("Failed to marshal source hook configuration: %v", err)
}
err = json.Unmarshal(b, target)
if err != nil {
log.Entry().Warningf("Failed to retrieve hook configuration: %v", err)
}
}
}
var errIncompatibleTypes = fmt.Errorf("incompatible types")
func checkTypes(config map[string]interface{}, options interface{}) map[string]interface{} {
optionsType := getStepOptionsStructType(options)
for paramName := range config {
optionsField := findStructFieldByJSONTag(paramName, optionsType)
if optionsField == nil {
continue
}
if config[paramName] == nil {
// There is a key, but no value. This can result from merging values from the CPE.
continue
}
paramValueType := reflect.ValueOf(config[paramName])
if optionsField.Type.Kind() == paramValueType.Kind() {
// Types already match, nothing to do
continue
}
var typeError error = nil
switch paramValueType.Kind() {
case reflect.String:
typeError = convertValueFromString(config, optionsField, paramName, paramValueType.String())
case reflect.Float32, reflect.Float64:
typeError = convertValueFromFloat(config, optionsField, paramName, paramValueType.Float())
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
typeError = convertValueFromInt(config, optionsField, paramName, paramValueType.Int())
default:
log.Entry().Warnf("Config value for '%s' is of unexpected type %s, expected %s. "+
"The value may be ignored as a result. To avoid any risk, specify this value with explicit type.",
paramName, paramValueType.Kind(), optionsField.Type.Kind())
}
if typeError != nil {
typeError = fmt.Errorf("config value for '%s' is of unexpected type %s, expected %s: %w",
paramName, paramValueType.Kind(), optionsField.Type.Kind(), typeError)
log.SetErrorCategory(log.ErrorConfiguration)
log.Entry().WithError(typeError).Fatal("type error in configuration")
}
}
return config
}
func convertValueFromString(config map[string]interface{}, optionsField *reflect.StructField, paramName, paramValue string) error {
switch optionsField.Type.Kind() {
case reflect.Slice, reflect.Array:
// Could do automatic conversion for those types in theory,
// but that might obscure what really happens in error cases.
return fmt.Errorf("expected type to be a list (or slice, or array) but got string")
case reflect.Bool:
// Sensible to convert strings "true"/"false" to respective boolean values as it is
// common practice to write booleans as string in yaml files.
paramValue = strings.ToLower(paramValue)
if paramValue == "true" {
config[paramName] = true
return nil
} else if paramValue == "false" {
config[paramName] = false
return nil
}
}
return errIncompatibleTypes
}
func convertValueFromFloat(config map[string]interface{}, optionsField *reflect.StructField, paramName string, paramValue float64) error {
switch optionsField.Type.Kind() {
case reflect.String:
val := strconv.FormatFloat(paramValue, 'f', -1, 64)
// if Sprinted value and val are equal, we can be pretty sure that the result fits
// for very large numbers for example an exponential format is printed
if val == fmt.Sprint(paramValue) {
config[paramName] = val
return nil
}
// allow float numbers containing a decimal separator
if strings.Contains(val, ".") {
config[paramName] = val
return nil
}
// if now no decimal separator is available we cannot be sure that the result is correct:
// long numbers like e.g. 73554900100200011600 will not be represented correctly after reading the yaml
// thus we cannot assume that the string is correct.
// short numbers will be handled as int anyway
return errIncompatibleTypes
case reflect.Float32:
config[paramName] = float32(paramValue)
return nil
case reflect.Float64:
config[paramName] = paramValue
return nil
case reflect.Int:
// Treat as type-mismatch only in case the conversion would be lossy.
// In that case, the json.Unmarshall() would indeed just drop it, so we want to fail.
if float64(int(paramValue)) == paramValue {
config[paramName] = int(paramValue)
return nil
}
}
return errIncompatibleTypes
}
func convertValueFromInt(config map[string]interface{}, optionsField *reflect.StructField, paramName string, paramValue int64) error {
switch optionsField.Type.Kind() {
case reflect.String:
config[paramName] = strconv.FormatInt(paramValue, 10)
return nil
case reflect.Float32:
config[paramName] = float32(paramValue)
return nil
case reflect.Float64:
config[paramName] = float64(paramValue)
return nil
}
return errIncompatibleTypes
}
func findStructFieldByJSONTag(tagName string, optionsType reflect.Type) *reflect.StructField {
for i := 0; i < optionsType.NumField(); i++ {
field := optionsType.Field(i)
tag := field.Tag.Get("json")
if tagName == tag || tagName+",omitempty" == tag {
return &field
}
}
return nil
}
func getStepOptionsStructType(stepOptions interface{}) reflect.Type {
typedOptions := reflect.ValueOf(stepOptions)
if typedOptions.Kind() == reflect.Ptr {
typedOptions = typedOptions.Elem()
}
return typedOptions.Type()
}
func getProjectConfigFile(name string) string {
var altName string
if ext := filepath.Ext(name); ext == ".yml" {
altName = fmt.Sprintf("%v.yaml", strings.TrimSuffix(name, ext))
} else if ext == "yaml" {
altName = fmt.Sprintf("%v.yml", strings.TrimSuffix(name, ext))
}
fileExists, _ := piperutils.FileExists(name)
altExists, _ := piperutils.FileExists(altName)
// configured filename will always take precedence, even if not existing
if !fileExists && altExists {
return altName
}
return name
}
func mergeResourceParameters(resParams ...map[string]interface{}) map[string]interface{} {
result := make(map[string]interface{})
for _, m := range resParams {
for k, v := range m {
result[k] = v
}
}
return result
}