From dea96a3ba0b5b52e2bba024e4b5ee7fe65b003dc Mon Sep 17 00:00:00 2001 From: lndrschlz Date: Wed, 17 Mar 2021 08:08:33 +0100 Subject: [PATCH] feat(newmanExecute): golang implmementation for newmanExecute (#2513) * Automates first parts of newmanExecute.groovy Signed-off-by: Fabian Reh * Adds newman installation Signed-off-by: Fabian Reh * Removes warning Signed-off-by: Fabian Reh * makes tests robust for later shell calls Signed-off-by: Fabian Reh * Adds version logging Signed-off-by: Fabian Reh * Adds tests for version logging Signed-off-by: Fabian Reh * Adds newman shell execution Signed-off-by: Fabian Reh * Prepare cloud foundry apps with secrets handling Signed-off-by: Fabian Reh * Adds further process to CF Utils Signed-off-by: Fabian Reh * Fixes unit test Signed-off-by: Fabian Reh * Adds error category Signed-off-by: Fabian Reh * Add fix to execute step locally Currently only tested on windows machine locally in powershell. Signed-off-by: Fabian Reh * Adapt unit test to fix of runCommand Signed-off-by: Fabian Reh * refactored golang step to newmanExecute * wip * added test config * refactored newmanExecute groovy wrapper step * exclude newmanExecute from common step test * cleaups * add credential support * fix groovy credential providing * add import * add stageName * define script * remove unused vars * add import * fix iterator ref * golang secret handling and cleanups * wip * wip * wip * update go step * implement cf credential proposal * testRepository functionality implemented * register secrets to logger * add missing dependecies * test xsuaa credential handling * wip * wip * cleanups * add import * remove mandatory params * add container definition * test runCommand * test runCommand * fix npm path * fix npm path * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * added newmanEnvironment to templating * wip * use env and globals params in runCommand when no templating * fix condition * wip * reverted config edit * updated documentation * install with shell * wip * wip * fix tests * refactor tests * wip * remove old test * wip * escape go tmpl * escape go tmpl * fix defaults * add doc comment * remove test case * refactored newman commands * add cli reporter * refactor options * mock os getenv and fix all tests * refactoring and doc update * go generate * small refactor * spelling * fix newman doc * remove MaskPasswords wrapper; fix stash bug; * docu fix Co-authored-by: Fabian Reh --- cmd/metadata_generated.go | 1 + cmd/newmanExecute.go | 217 ++++++++++++++ cmd/newmanExecute_generated.go | 181 ++++++++++++ cmd/newmanExecute_generated_test.go | 17 ++ cmd/newmanExecute_test.go | 324 +++++++++++++++++++++ cmd/piper.go | 1 + documentation/docs/steps/newmanExecute.md | 28 +- resources/default_pipeline_environment.yml | 10 - resources/metadata/newmanExecute.yaml | 86 ++++++ test/groovy/CommonStepsTest.groovy | 2 + test/groovy/NewmanExecuteTest.groovy | 206 ------------- vars/newmanExecute.groovy | 220 +++----------- vars/uiVeri5ExecuteTests.groovy | 2 +- 13 files changed, 901 insertions(+), 394 deletions(-) create mode 100644 cmd/newmanExecute.go create mode 100644 cmd/newmanExecute_generated.go create mode 100644 cmd/newmanExecute_generated_test.go create mode 100644 cmd/newmanExecute_test.go create mode 100644 resources/metadata/newmanExecute.yaml delete mode 100644 test/groovy/NewmanExecuteTest.groovy diff --git a/cmd/metadata_generated.go b/cmd/metadata_generated.go index 6734f21a1..6cea7981d 100644 --- a/cmd/metadata_generated.go +++ b/cmd/metadata_generated.go @@ -61,6 +61,7 @@ func GetAllStepMetadata() map[string]config.StepData { "mavenExecuteIntegration": mavenExecuteIntegrationMetadata(), "mavenExecuteStaticCodeChecks": mavenExecuteStaticCodeChecksMetadata(), "mtaBuild": mtaBuildMetadata(), + "newmanExecute": newmanExecuteMetadata(), "nexusUpload": nexusUploadMetadata(), "npmExecuteLint": npmExecuteLintMetadata(), "npmExecuteScripts": npmExecuteScriptsMetadata(), diff --git a/cmd/newmanExecute.go b/cmd/newmanExecute.go new file mode 100644 index 000000000..1858b2709 --- /dev/null +++ b/cmd/newmanExecute.go @@ -0,0 +1,217 @@ +package cmd + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "strings" + "text/template" + + "github.com/SAP/jenkins-library/pkg/command" + "github.com/SAP/jenkins-library/pkg/log" + "github.com/SAP/jenkins-library/pkg/piperutils" + "github.com/SAP/jenkins-library/pkg/telemetry" + "github.com/pkg/errors" +) + +type newmanExecuteUtils interface { + Glob(pattern string) (matches []string, err error) + RunExecutable(executable string, params ...string) error + Getenv(key string) string +} + +type newmanExecuteUtilsBundle struct { + *command.Command + *piperutils.Files +} + +func newNewmanExecuteUtils() newmanExecuteUtils { + utils := newmanExecuteUtilsBundle{ + Command: &command.Command{}, + Files: &piperutils.Files{}, + } + // Reroute command output to logging framework + utils.Stdout(log.Writer()) + utils.Stderr(log.Writer()) + return &utils +} + +func newmanExecute(config newmanExecuteOptions, _ *telemetry.CustomData) { + utils := newNewmanExecuteUtils() + + err := runNewmanExecute(&config, utils) + if err != nil { + log.Entry().WithError(err).Fatal("step execution failed") + } +} + +func runNewmanExecute(config *newmanExecuteOptions, utils newmanExecuteUtils) error { + if config.NewmanRunCommand != "" { + log.Entry().Warn("found configuration for deprecated parameter newmanRunCommand, please use runOptions instead") + log.Entry().Warn("setting runOptions to value of deprecated parameter newmanRunCommand") + config.RunOptions = strings.Split(config.NewmanRunCommand, " ") + } + + collectionList, err := utils.Glob(config.NewmanCollection) + if err != nil { + log.SetErrorCategory(log.ErrorConfiguration) + return errors.Wrapf(err, "Could not execute global search for '%v'", config.NewmanCollection) + } + + if collectionList == nil { + log.SetErrorCategory(log.ErrorConfiguration) + return fmt.Errorf("no collection found with pattern '%v'", config.NewmanCollection) + } + log.Entry().Infof("Found the following newman collections: %v", collectionList) + + err = logVersions(utils) + if err != nil { + return err + } + + err = installNewman(config.NewmanInstallCommand, utils) + if err != nil { + return err + } + + // resolve environment and globals if not covered by templating + options := resolveOptions(config) + + for _, collection := range collectionList { + runOptions := []string{} + runOptions, err := resolveTemplate(config, collection) + if err != nil { + return err + } + + commandSecrets := handleCfAppCredentials(config) + + runOptions = append(runOptions, options...) + runOptions = append(runOptions, commandSecrets...) + + if !config.FailOnError { + runOptions = append(runOptions, "--suppress-exit-code") + } + + newmanPath := filepath.Join(utils.Getenv("HOME"), "/.npm-global/bin/newman") + err = utils.RunExecutable(newmanPath, runOptions...) + if err != nil { + log.SetErrorCategory(log.ErrorService) + return errors.Wrap(err, "The execution of the newman tests failed, see the log for details.") + } + } + return nil +} + +func logVersions(utils newmanExecuteUtils) error { + err := utils.RunExecutable("node", "--version") + if err != nil { + log.SetErrorCategory(log.ErrorInfrastructure) + return errors.Wrap(err, "error logging node version") + } + err = utils.RunExecutable("npm", "--version") + if err != nil { + log.SetErrorCategory(log.ErrorInfrastructure) + return errors.Wrap(err, "error logging npm version") + } + return nil +} + +func installNewman(newmanInstallCommand string, utils newmanExecuteUtils) error { + installCommandTokens := strings.Split(newmanInstallCommand, " ") + installCommandTokens = append(installCommandTokens, "--prefix=~/.npm-global") + err := utils.RunExecutable(installCommandTokens[0], installCommandTokens[1:]...) + if err != nil { + log.SetErrorCategory(log.ErrorConfiguration) + return errors.Wrap(err, "error installing newman") + } + return nil +} + +func resolveOptions(config *newmanExecuteOptions) []string { + options := []string{} + if config.NewmanEnvironment != "" && !contains(config.RunOptions, "{{.Config.NewmanEnvironment}}") { + options = append(options, "--environment") + options = append(options, config.NewmanEnvironment) + } + if config.NewmanGlobals != "" && !contains(config.RunOptions, "{{.Config.NewmanGlobals}}") { + options = append(options, "--globals") + options = append(options, config.NewmanGlobals) + } + return options +} + +func resolveTemplate(config *newmanExecuteOptions, collection string) ([]string, error) { + cmd := []string{} + collectionDisplayName := defineCollectionDisplayName(collection) + + type TemplateConfig struct { + Config interface{} + CollectionDisplayName string + NewmanCollection string + } + + for _, runOption := range config.RunOptions { + templ, err := template.New("template").Parse(runOption) + if err != nil { + log.SetErrorCategory(log.ErrorConfiguration) + return nil, errors.Wrap(err, "could not parse newman command template") + } + buf := new(bytes.Buffer) + err = templ.Execute(buf, TemplateConfig{ + Config: config, + CollectionDisplayName: collectionDisplayName, + NewmanCollection: collection, + }) + if err != nil { + log.SetErrorCategory(log.ErrorConfiguration) + return nil, errors.Wrap(err, "error on executing template") + } + cmd = append(cmd, buf.String()) + } + + return cmd, nil +} + +func defineCollectionDisplayName(collection string) string { + replacedSeparators := strings.Replace(collection, string(filepath.Separator), "_", -1) + displayName := strings.Split(replacedSeparators, ".") + if displayName[0] == "" && len(displayName) >= 2 { + return displayName[1] + } + return displayName[0] +} + +func handleCfAppCredentials(config *newmanExecuteOptions) []string { + commandSecrets := []string{} + if len(config.CfAppsWithSecrets) > 0 { + for _, appName := range config.CfAppsWithSecrets { + var clientID, clientSecret string + clientID = os.Getenv("PIPER_NEWMANEXECUTE_" + appName + "_clientid") + clientSecret = os.Getenv("PIPER_NEWMANEXECUTE_" + appName + "_clientsecret") + if clientID != "" && clientSecret != "" { + log.RegisterSecret(clientSecret) + secretVar := fmt.Sprintf("--env-var %v_clientid=%v --env-var %v_clientsecret=%v", appName, clientID, appName, clientSecret) + commandSecrets = append(commandSecrets, strings.Split(secretVar, " ")...) + log.Entry().Infof("secrets found for app %v and forwarded to newman as --env-var parameter", appName) + } else { + log.Entry().Errorf("cannot fetch secrets from environment variables for app %v", appName) + } + } + } + return commandSecrets +} + +func contains(slice []string, substr string) bool { + for _, e := range slice { + if strings.Contains(e, substr) { + return true + } + } + return false +} + +func (utils newmanExecuteUtilsBundle) Getenv(key string) string { + return os.Getenv(key) +} diff --git a/cmd/newmanExecute_generated.go b/cmd/newmanExecute_generated.go new file mode 100644 index 000000000..4b79248f2 --- /dev/null +++ b/cmd/newmanExecute_generated.go @@ -0,0 +1,181 @@ +// Code generated by piper's step-generator. DO NOT EDIT. + +package cmd + +import ( + "fmt" + "os" + "time" + + "github.com/SAP/jenkins-library/pkg/config" + "github.com/SAP/jenkins-library/pkg/log" + "github.com/SAP/jenkins-library/pkg/telemetry" + "github.com/spf13/cobra" +) + +type newmanExecuteOptions struct { + NewmanCollection string `json:"newmanCollection,omitempty"` + NewmanRunCommand string `json:"newmanRunCommand,omitempty"` + RunOptions []string `json:"runOptions,omitempty"` + NewmanInstallCommand string `json:"newmanInstallCommand,omitempty"` + NewmanEnvironment string `json:"newmanEnvironment,omitempty"` + NewmanGlobals string `json:"newmanGlobals,omitempty"` + FailOnError bool `json:"failOnError,omitempty"` + CfAppsWithSecrets []string `json:"cfAppsWithSecrets,omitempty"` +} + +// NewmanExecuteCommand Installs newman and executes specified newman collections. +func NewmanExecuteCommand() *cobra.Command { + const STEP_NAME = "newmanExecute" + + metadata := newmanExecuteMetadata() + var stepConfig newmanExecuteOptions + var startTime time.Time + + var createNewmanExecuteCmd = &cobra.Command{ + Use: STEP_NAME, + Short: "Installs newman and executes specified newman collections.", + Long: `This script executes [Postman](https://www.getpostman.com) tests from a collection via the [Newman](https://www.getpostman.com/docs/v6/postman/collection_runs/command_line_integration_with_newman) command line tool.`, + PreRunE: func(cmd *cobra.Command, _ []string) error { + startTime = time.Now() + log.SetStepName(STEP_NAME) + log.SetVerbose(GeneralConfig.Verbose) + + path, _ := os.Getwd() + fatalHook := &log.FatalHook{CorrelationID: GeneralConfig.CorrelationID, Path: path} + log.RegisterHook(fatalHook) + + err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile) + if err != nil { + log.SetErrorCategory(log.ErrorConfiguration) + return err + } + + if len(GeneralConfig.HookConfig.SentryConfig.Dsn) > 0 { + sentryHook := log.NewSentryHook(GeneralConfig.HookConfig.SentryConfig.Dsn, GeneralConfig.CorrelationID) + log.RegisterHook(&sentryHook) + } + + return nil + }, + Run: func(_ *cobra.Command, _ []string) { + telemetryData := telemetry.CustomData{} + telemetryData.ErrorCode = "1" + handler := func() { + config.RemoveVaultSecretFiles() + telemetryData.Duration = fmt.Sprintf("%v", time.Since(startTime).Milliseconds()) + telemetryData.ErrorCategory = log.GetErrorCategory().String() + telemetry.Send(&telemetryData) + } + log.DeferExitHandler(handler) + defer handler() + telemetry.Initialize(GeneralConfig.NoTelemetry, STEP_NAME) + newmanExecute(stepConfig, &telemetryData) + telemetryData.ErrorCode = "0" + log.Entry().Info("SUCCESS") + }, + } + + addNewmanExecuteFlags(createNewmanExecuteCmd, &stepConfig) + return createNewmanExecuteCmd +} + +func addNewmanExecuteFlags(cmd *cobra.Command, stepConfig *newmanExecuteOptions) { + cmd.Flags().StringVar(&stepConfig.NewmanCollection, "newmanCollection", `**/*.postman_collection.json`, "The test collection that should be executed. This could also be a file pattern.") + cmd.Flags().StringVar(&stepConfig.NewmanRunCommand, "newmanRunCommand", os.Getenv("PIPER_newmanRunCommand"), "+++ Deprecated +++ Please use list parameter `runOptions` instead.") + cmd.Flags().StringSliceVar(&stepConfig.RunOptions, "runOptions", []string{`run`, `{{.NewmanCollection}}`, `--reporters`, `cli,junit,html`, `--reporter-junit-export`, `target/newman/TEST-{{.CollectionDisplayName}}.xml`, `--reporter-html-export`, `target/newman/TEST-{{.CollectionDisplayName}}.html`}, "The newman command that will be executed inside the docker container.") + cmd.Flags().StringVar(&stepConfig.NewmanInstallCommand, "newmanInstallCommand", `npm install newman newman-reporter-html --global --quiet`, "The shell command that will be executed inside the docker container to install Newman.") + cmd.Flags().StringVar(&stepConfig.NewmanEnvironment, "newmanEnvironment", os.Getenv("PIPER_newmanEnvironment"), "Specify an environment file path or URL. Environments provide a set of variables that one can use within collections.") + cmd.Flags().StringVar(&stepConfig.NewmanGlobals, "newmanGlobals", os.Getenv("PIPER_newmanGlobals"), "Specify the file path or URL for global variables. Global variables are similar to environment variables but have a lower precedence and can be overridden by environment variables having the same name.") + cmd.Flags().BoolVar(&stepConfig.FailOnError, "failOnError", true, "Defines the behavior, in case tests fail.") + cmd.Flags().StringSliceVar(&stepConfig.CfAppsWithSecrets, "cfAppsWithSecrets", []string{}, "List of CloudFoundry apps with secrets") + +} + +// retrieve step metadata +func newmanExecuteMetadata() config.StepData { + var theMetaData = config.StepData{ + Metadata: config.StepMetadata{ + Name: "newmanExecute", + Aliases: []config.Alias{}, + Description: "Installs newman and executes specified newman collections.", + }, + Spec: config.StepSpec{ + Inputs: config.StepInputs{ + Resources: []config.StepResources{ + {Name: "tests", Type: "stash"}, + }, + Parameters: []config.StepParameters{ + { + Name: "newmanCollection", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: false, + Aliases: []config.Alias{}, + }, + { + Name: "newmanRunCommand", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: false, + Aliases: []config.Alias{}, + }, + { + Name: "runOptions", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "[]string", + Mandatory: false, + Aliases: []config.Alias{}, + }, + { + Name: "newmanInstallCommand", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: false, + Aliases: []config.Alias{}, + }, + { + Name: "newmanEnvironment", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: false, + Aliases: []config.Alias{}, + }, + { + Name: "newmanGlobals", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: false, + Aliases: []config.Alias{}, + }, + { + Name: "failOnError", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "bool", + Mandatory: false, + Aliases: []config.Alias{}, + }, + { + Name: "cfAppsWithSecrets", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "[]string", + Mandatory: false, + Aliases: []config.Alias{}, + }, + }, + }, + Containers: []config.Container{ + {Name: "newman", Image: "node:lts-stretch", WorkingDir: "/home/node"}, + }, + }, + } + return theMetaData +} diff --git a/cmd/newmanExecute_generated_test.go b/cmd/newmanExecute_generated_test.go new file mode 100644 index 000000000..bdab743bd --- /dev/null +++ b/cmd/newmanExecute_generated_test.go @@ -0,0 +1,17 @@ +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewmanExecuteCommand(t *testing.T) { + t.Parallel() + + testCmd := NewmanExecuteCommand() + + // only high level testing performed - details are tested in step generation procedure + assert.Equal(t, "newmanExecute", testCmd.Use, "command name incorrect") + +} diff --git a/cmd/newmanExecute_test.go b/cmd/newmanExecute_test.go new file mode 100644 index 000000000..42cab7569 --- /dev/null +++ b/cmd/newmanExecute_test.go @@ -0,0 +1,324 @@ +package cmd + +import ( + "path/filepath" + "strings" + "testing" + + sliceUtils "github.com/SAP/jenkins-library/pkg/piperutils" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +type executedExecutables struct { + executable string + params []string +} + +type newmanExecuteMockUtils struct { + // *mock.ExecMockRunner + // *mock.FilesMock + errorOnGlob bool + errorOnNewmanInstall bool + errorOnRunShell bool + errorOnNewmanExecution bool + errorOnLoggingNode bool + errorOnLoggingNpm bool + executedExecutables []executedExecutables + filesToFind []string + commandIndex int +} + +func newNewmanExecuteMockUtils() newmanExecuteMockUtils { + return newmanExecuteMockUtils{ + filesToFind: []string{"localFile.json", "localFile2.json"}, + } +} + +func TestRunNewmanExecute(t *testing.T) { + t.Parallel() + + allFineConfig := newmanExecuteOptions{ + NewmanCollection: "**.json", + NewmanEnvironment: "env.json", + NewmanGlobals: "globals.json", + NewmanInstallCommand: "npm install newman --global --quiet", + NewmanRunCommand: "run {{.NewmanCollection}} --environment {{.Config.NewmanEnvironment}} --globals {{.Config.NewmanGlobals}} --reporters junit,html --reporter-junit-export target/newman/TEST-{{.CollectionDisplayName}}.xml --reporter-html-export target/newman/TEST-{{.CollectionDisplayName}}.html", + } + + t.Run("happy path", func(t *testing.T) { + t.Parallel() + // init + + utils := newNewmanExecuteMockUtils() + + // test + err := runNewmanExecute(&allFineConfig, &utils) + + // assert + assert.NoError(t, err) + assert.Contains(t, utils.executedExecutables, executedExecutables{executable: "node", params: []string{"--version"}}) + assert.Contains(t, utils.executedExecutables, executedExecutables{executable: "npm", params: []string{"--version"}}) + assert.Contains(t, utils.executedExecutables, executedExecutables{executable: "npm", params: []string{"install", "newman", "--global", "--quiet", "--prefix=~/.npm-global"}}) + assert.Contains(t, utils.executedExecutables, executedExecutables{executable: "/home/node/.npm-global/bin/newman", params: []string{"run", "localFile.json", "--environment", "env.json", "--globals", "globals.json", "--reporters", "junit,html", "--reporter-junit-export", "target/newman/TEST-localFile.xml", "--reporter-html-export", "target/newman/TEST-localFile.html", "--suppress-exit-code"}}) + assert.Contains(t, utils.executedExecutables, executedExecutables{executable: "/home/node/.npm-global/bin/newman", params: []string{"run", "localFile2.json", "--environment", "env.json", "--globals", "globals.json", "--reporters", "junit,html", "--reporter-junit-export", "target/newman/TEST-localFile2.xml", "--reporter-html-export", "target/newman/TEST-localFile2.html", "--suppress-exit-code"}}) + }) + + t.Run("happy path with fail on error", func(t *testing.T) { + t.Parallel() + // init + + utils := newNewmanExecuteMockUtils() + fineConfig := allFineConfig + fineConfig.FailOnError = true + + // test + err := runNewmanExecute(&fineConfig, &utils) + + // assert + assert.NoError(t, err) + assert.Contains(t, utils.executedExecutables, executedExecutables{executable: "node", params: []string{"--version"}}) + assert.Contains(t, utils.executedExecutables, executedExecutables{executable: "npm", params: []string{"--version"}}) + assert.Contains(t, utils.executedExecutables, executedExecutables{executable: "npm", params: []string{"install", "newman", "--global", "--quiet", "--prefix=~/.npm-global"}}) + assert.Contains(t, utils.executedExecutables, executedExecutables{executable: "/home/node/.npm-global/bin/newman", params: []string{"run", "localFile.json", "--environment", "env.json", "--globals", "globals.json", "--reporters", "junit,html", "--reporter-junit-export", "target/newman/TEST-localFile.xml", "--reporter-html-export", "target/newman/TEST-localFile.html"}}) + assert.Contains(t, utils.executedExecutables, executedExecutables{executable: "/home/node/.npm-global/bin/newman", params: []string{"run", "localFile2.json", "--environment", "env.json", "--globals", "globals.json", "--reporters", "junit,html", "--reporter-junit-export", "target/newman/TEST-localFile2.xml", "--reporter-html-export", "target/newman/TEST-localFile2.html"}}) + }) + + t.Run("error on newman execution", func(t *testing.T) { + t.Parallel() + // init + + utils := newNewmanExecuteMockUtils() + utils.errorOnNewmanExecution = true + + // test + err := runNewmanExecute(&allFineConfig, &utils) + + // assert + assert.EqualError(t, err, "The execution of the newman tests failed, see the log for details.: error on newman execution") + }) + + t.Run("error on newman installation", func(t *testing.T) { + t.Parallel() + // init + + utils := newNewmanExecuteMockUtils() + utils.errorOnNewmanInstall = true + + // test + err := runNewmanExecute(&allFineConfig, &utils) + + // assert + assert.EqualError(t, err, "error installing newman: error on newman install") + }) + + t.Run("error on npm version logging", func(t *testing.T) { + t.Parallel() + // init + + utils := newNewmanExecuteMockUtils() + utils.errorOnLoggingNpm = true + + // test + err := runNewmanExecute(&allFineConfig, &utils) + + // assert + assert.EqualError(t, err, "error logging npm version: error on RunExecutable") + }) + + t.Run("error on template resolution", func(t *testing.T) { + t.Parallel() + // init + + utils := newNewmanExecuteMockUtils() + config := allFineConfig + config.NewmanRunCommand = "this is my erroneous command {{.collectionDisplayName}" + + // test + err := runNewmanExecute(&config, &utils) + + // assert + assert.EqualError(t, err, "could not parse newman command template: template: template:1: unexpected \"}\" in operand") + }) + + t.Run("error on file search", func(t *testing.T) { + t.Parallel() + // init + + utils := newNewmanExecuteMockUtils() + utils.filesToFind = nil + + // test + err := runNewmanExecute(&allFineConfig, &utils) + + // assert + assert.EqualError(t, err, "no collection found with pattern '**.json'") + }) + + t.Run("no newman file", func(t *testing.T) { + t.Parallel() + // init + + utils := newNewmanExecuteMockUtils() + utils.errorOnGlob = true + + // test + err := runNewmanExecute(&allFineConfig, &utils) + + // assert + assert.EqualError(t, err, "Could not execute global search for '**.json': error on Glob") + }) +} + +func TestDefineCollectionDisplayName(t *testing.T) { + t.Parallel() + + t.Run("normal path", func(t *testing.T) { + t.Parallel() + + path := filepath.Join("dir1", "dir2", "fancyFile.txt") + result := defineCollectionDisplayName(path) + assert.Equal(t, "dir1_dir2_fancyFile", result) + }) + + t.Run("directory", func(t *testing.T) { + t.Parallel() + + path := filepath.Join("dir1", "dir2", "dir3") + result := defineCollectionDisplayName(path) + assert.Equal(t, "dir1_dir2_dir3", result) + }) + + t.Run("directory with dot prefix", func(t *testing.T) { + t.Parallel() + + path := filepath.Join(".dir1", "dir2", "dir3", "file.json") + result := defineCollectionDisplayName(path) + assert.Equal(t, "dir1_dir2_dir3_file", result) + }) + + t.Run("empty path", func(t *testing.T) { + t.Parallel() + + path := filepath.Join(".") + result := defineCollectionDisplayName(path) + assert.Equal(t, "", result) + }) +} + +func TestResolveTemplate(t *testing.T) { + t.Parallel() + + t.Run("nothing to replace", func(t *testing.T) { + t.Parallel() + + // config := newmanExecuteOptions{NewmanRunCommand: "this is my fancy command"} + config := newmanExecuteOptions{RunOptions: []string{"this", "is", "my", "fancy", "command"}} + + cmd, err := resolveTemplate(&config, "collectionsDisplayName") + assert.NoError(t, err) + assert.Equal(t, []string{"this", "is", "my", "fancy", "command"}, cmd) + }) + + t.Run("replace display name", func(t *testing.T) { + t.Parallel() + + config := newmanExecuteOptions{RunOptions: []string{"this", "is", "my", "fancy", "command", "{{.CollectionDisplayName}}"}} + + cmd, err := resolveTemplate(&config, "theDisplayName") + assert.NoError(t, err) + assert.Equal(t, []string{"this", "is", "my", "fancy", "command", "theDisplayName"}, cmd) + }) + + t.Run("error when parameter cannot be resolved", func(t *testing.T) { + t.Parallel() + + config := newmanExecuteOptions{RunOptions: []string{"this", "is", "my", "fancy", "command", "{{.collectionDisplayName}}"}} + + _, err := resolveTemplate(&config, "theDisplayName") + assert.EqualError(t, err, "error on executing template: template: template:1:2: executing \"template\" at <.collectionDisplayName>: can't evaluate field collectionDisplayName in type cmd.TemplateConfig") + }) + + t.Run("error when template cannot be parsed", func(t *testing.T) { + t.Parallel() + + config := newmanExecuteOptions{RunOptions: []string{"this", "is", "my", "fancy", "command", "{{.collectionDisplayName}"}} + + _, err := resolveTemplate(&config, "theDisplayName") + assert.EqualError(t, err, "could not parse newman command template: template: template:1: unexpected \"}\" in operand") + }) +} + +func TestLogVersions(t *testing.T) { + t.Parallel() + + t.Run("happy path", func(t *testing.T) { + utils := newNewmanExecuteMockUtils() + + err := logVersions(&utils) + assert.NoError(t, err) + assert.Contains(t, utils.executedExecutables, executedExecutables{executable: "npm", params: []string{"--version"}}) + }) + + t.Run("error in node execution", func(t *testing.T) { + utils := newNewmanExecuteMockUtils() + utils.errorOnLoggingNode = true + + err := logVersions(&utils) + assert.EqualError(t, err, "error logging node version: error on RunExecutable") + }) + + t.Run("error in npm execution", func(t *testing.T) { + utils := newNewmanExecuteMockUtils() + utils.errorOnLoggingNpm = true + + err := logVersions(&utils) + assert.EqualError(t, err, "error logging npm version: error on RunExecutable") + assert.Contains(t, utils.executedExecutables, executedExecutables{executable: "node", params: []string{"--version"}}) + }) +} + +func (e *newmanExecuteMockUtils) Glob(string) (matches []string, err error) { + if e.errorOnGlob { + return nil, errors.New("error on Glob") + } + + return e.filesToFind, nil +} + +func (e *newmanExecuteMockUtils) RunExecutable(executable string, params ...string) error { + if e.errorOnRunShell { + return errors.New("error on RunExecutable") + } + if e.errorOnLoggingNode && executable == "node" && params[0] == "--version" { + return errors.New("error on RunExecutable") + } + if e.errorOnLoggingNpm && executable == "npm" && params[0] == "--version" { + return errors.New("error on RunExecutable") + } + if e.errorOnNewmanExecution && strings.Contains(executable, "newman") { + return errors.New("error on newman execution") + } + if e.errorOnNewmanInstall && sliceUtils.ContainsString(params, "install") { + return errors.New("error on newman install") + } + + length := len(e.executedExecutables) + if length < e.commandIndex+1 { + e.executedExecutables = append(e.executedExecutables, executedExecutables{}) + length++ + } + + e.executedExecutables[length-1].executable = executable + e.executedExecutables[length-1].params = params + e.commandIndex++ + + return nil +} + +func (e *newmanExecuteMockUtils) Getenv(key string) string { + if key == "HOME" { + return "/home/node" + } + return "" +} diff --git a/cmd/piper.go b/cmd/piper.go index db93ffd09..22cb7b03f 100644 --- a/cmd/piper.go +++ b/cmd/piper.go @@ -127,6 +127,7 @@ func Execute() { rootCmd.AddCommand(VaultRotateSecretIdCommand()) rootCmd.AddCommand(CheckChangeInDevelopmentCommand()) rootCmd.AddCommand(TransportRequestUploadCTSCommand()) + rootCmd.AddCommand(NewmanExecuteCommand()) rootCmd.AddCommand(IntegrationArtifactDeployCommand()) rootCmd.AddCommand(TransportRequestUploadSOLMANCommand()) rootCmd.AddCommand(IntegrationArtifactUpdateConfigurationCommand()) diff --git a/documentation/docs/steps/newmanExecute.md b/documentation/docs/steps/newmanExecute.md index e744d5df4..44e473988 100644 --- a/documentation/docs/steps/newmanExecute.md +++ b/documentation/docs/steps/newmanExecute.md @@ -1,5 +1,8 @@ # ${docGenStepName} +!!! warning "Deprecation notice" + Details of changes after the step migration to a golang can be found [below](#exceptions). + ## ${docGenDescription} ## Prerequisites @@ -18,7 +21,30 @@ Step uses `dockerExecute` inside. ## Exceptions -none +The step has been migrated into a golang-based step. The following release notes belong to the new implementation: + +- **newmanRunCommand**: + +The parameter `newmanRunCommand` is deprecated by now and is replaced by list parameter `runOptions`. For backward compatibility, the `newmanRunCommand` parameter will still be used if configured. Nevertheless, using this parameter can break the step in some cases, e.g. when spaces are used in single quoted strings like spaces in file names. Also Groovy Templating is deprecated and now replaced by Go Templating. The example show the required changes: + +```yaml +# deprecated groovy default +newmanRunCommand: "run '${config.newmanCollection}' --environment '${config.newmanEnvironment}' --globals '${config.newmanGlobals}' --reporters junit,html --reporter-junit-export 'target/newman/TEST-${collectionDisplayName}.xml' --reporter-html-export 'target/newman/TEST-${collectionDisplayName}.html'" +``` + +```yaml +# new run options using golang templating +{{`runOptions: ["run", "{{.NewmanCollection}}", "--environment", "{{.Config.NewmanEnvironment}}", "--globals", "{{.Config.NewmanGlobals}}", "--reporters", "junit,html", "--reporter-junit-export", "target/newman/TEST-{{.CollectionDisplayName}}.xml", "--reporter-html-export", "target/newman/TEST-{{.CollectionDisplayName}}.html"]`}} +``` + +If the following error occurs during the pipeline run, the `newmanRunCommand` is probably still configured with the deprecated groovy template syntax: +> info newmanExecute - error: collection could not be loaded +> info newmanExecute - unable to read data from file "${config.newmanCollection}" +> info newmanExecute - ENOENT: no such file or directory, open '${config.newmanCollection}' + +- **newmanEnvironment and newmanGlobals**: + +Referencing `newmanEnvironment` and `newmanGlobals` in the runOptions is redundant now. Both parameters are added to runCommand using `newmanEnvironment` and `newmanGlobals` from config when configured and not referenced by go templating using `"--environment", "{{`{{.Config.NewmanEnvironment}}`}}"` and `"--globals", "{{`{{.Config.NewmanGlobals}}`}}"` as shown above. ## Example diff --git a/resources/default_pipeline_environment.yml b/resources/default_pipeline_environment.yml index 6195c72b8..8da3459a9 100644 --- a/resources/default_pipeline_environment.yml +++ b/resources/default_pipeline_environment.yml @@ -358,16 +358,6 @@ steps: neoTargets: [] enableZeroDowntimeDeployment: false parallelExecution: false - newmanExecute: - dockerImage: 'node:lts-stretch' - failOnError: true - newmanCollection: '**/*.postman_collection.json' - newmanEnvironment: '' - newmanGlobals: '' - newmanInstallCommand: 'npm install newman newman-reporter-html --global --quiet' - newmanRunCommand: "run '${config.newmanCollection}' --environment '${config.newmanEnvironment}' --globals '${config.newmanGlobals}' --reporters junit,html --reporter-junit-export 'target/newman/TEST-${collectionDisplayName}.xml' --reporter-html-export 'target/newman/TEST-${collectionDisplayName}.html'" - stashContent: - - 'tests' npmExecute: dockerImage: 'node:lts-stretch' npmExecuteScripts: diff --git a/resources/metadata/newmanExecute.yaml b/resources/metadata/newmanExecute.yaml new file mode 100644 index 000000000..1fc452162 --- /dev/null +++ b/resources/metadata/newmanExecute.yaml @@ -0,0 +1,86 @@ +metadata: + name: newmanExecute + description: Installs newman and executes specified newman collections. + longDescription: | + This script executes [Postman](https://www.getpostman.com) tests from a collection via the [Newman](https://www.getpostman.com/docs/v6/postman/collection_runs/command_line_integration_with_newman) command line tool. +spec: + inputs: + resources: + - name: tests + type: stash + params: + - name: newmanCollection + description: The test collection that should be executed. This could also be a file pattern. + scope: + - PARAMETERS + - STAGES + - STEPS + type: string + default: '**/*.postman_collection.json' + - name: newmanRunCommand + description: +++ Deprecated +++ Please use list parameter `runOptions` instead. + scope: + - PARAMETERS + - STAGES + - STEPS + type: string + - name: runOptions + description: The newman command that will be executed inside the docker container. + scope: + - PARAMETERS + - STAGES + - STEPS + type: "[]string" + default: + - run + - "{{.NewmanCollection}}" + - --reporters + - cli,junit,html + - --reporter-junit-export + - target/newman/TEST-{{.CollectionDisplayName}}.xml + - --reporter-html-export + - target/newman/TEST-{{.CollectionDisplayName}}.html + - name: newmanInstallCommand + description: The shell command that will be executed inside the docker container to install Newman. + scope: + - PARAMETERS + - STAGES + - STEPS + type: string + default: npm install newman newman-reporter-html --global --quiet + - name: newmanEnvironment + description: Specify an environment file path or URL. Environments provide a set of variables that one can use within collections. + longDescription: see also [Newman docs](https://github.com/postmanlabs/newman#newman-run-collection-file-source-options) + scope: + - PARAMETERS + - STAGES + - STEPS + type: string + - name: newmanGlobals + description: Specify the file path or URL for global variables. Global variables are similar to environment variables but have a lower precedence and can be overridden by environment variables having the same name. + longDescription: see also [Newman docs](https://github.com/postmanlabs/newman#newman-run-collection-file-source-options) + scope: + - PARAMETERS + - STAGES + - STEPS + type: string + - name: failOnError + description: Defines the behavior, in case tests fail. + scope: + - PARAMETERS + - STAGES + - STEPS + type: bool + default: true + - name: cfAppsWithSecrets + description: List of CloudFoundry apps with secrets + longDescription: Define name array of cloud foundry apps deployed for which secrets (clientid and clientsecret) will be appended to the newman command that overrides the environment json entries (--env-var =${clientid} & --env-var =${clientsecret}) + scope: + - PARAMETERS + - STAGES + - STEPS + type: "[]string" + containers: + - name: newman + image: node:lts-stretch + workingDir: /home/node diff --git a/test/groovy/CommonStepsTest.groovy b/test/groovy/CommonStepsTest.groovy index 40e80a5ae..cc0369952 100644 --- a/test/groovy/CommonStepsTest.groovy +++ b/test/groovy/CommonStepsTest.groovy @@ -171,6 +171,8 @@ public class CommonStepsTest extends BasePiperTest{ 'kanikoExecute', //implementing new golang pattern without fields 'gitopsUpdateDeployment', //implementing new golang pattern without fields 'vaultRotateSecretId', //implementing new golang pattern without fields + 'deployIntegrationArtifact', //implementing new golang pattern without fields + 'newmanExecute', //implementing new golang pattern without fields 'whitesourceExecuteScan', //implementing new golang pattern without fields 'uiVeri5ExecuteTests', //implementing new golang pattern without fields 'integrationArtifactDeploy', //implementing new golang pattern without fields diff --git a/test/groovy/NewmanExecuteTest.groovy b/test/groovy/NewmanExecuteTest.groovy deleted file mode 100644 index 9d1808a88..000000000 --- a/test/groovy/NewmanExecuteTest.groovy +++ /dev/null @@ -1,206 +0,0 @@ -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.junit.Rule -import org.junit.rules.RuleChain - -import static org.hamcrest.Matchers.hasItem -import static org.hamcrest.Matchers.is -import static org.hamcrest.Matchers.containsString -import static org.hamcrest.Matchers.endsWith -import static org.hamcrest.Matchers.startsWith -import groovy.json.JsonSlurper - -import static org.junit.Assert.assertThat - -import util.BasePiperTest -import util.JenkinsStepRule -import util.JenkinsLoggingRule -import util.JenkinsReadYamlRule -import util.JenkinsShellCallRule -import util.JenkinsDockerExecuteRule -import util.Rules -import org.junit.rules.ExpectedException -import util.JenkinsCredentialsRule - -import com.sap.piper.Utils - -class NewmanExecuteTest extends BasePiperTest { - private ExpectedException thrown = ExpectedException.none() - private JenkinsStepRule stepRule = new JenkinsStepRule(this) - private JenkinsLoggingRule loggingRule = new JenkinsLoggingRule(this) - private JenkinsShellCallRule shellRule = new JenkinsShellCallRule(this) - private JenkinsDockerExecuteRule dockerExecuteRule = new JenkinsDockerExecuteRule(this) - private JenkinsCredentialsRule jenkinsCredentialsRule = new JenkinsCredentialsRule(this) - - @Rule - public RuleChain rules = Rules - .getCommonRules(this) - .around(new JenkinsReadYamlRule(this)) - .around(thrown) - .around(dockerExecuteRule) - .around(shellRule) - .around(jenkinsCredentialsRule) - .around(loggingRule) - .around(stepRule) // needs to be activated after dockerExecuteRule, otherwise executeDocker is not mocked - - def gitMap - - @Before - void init() throws Exception { - helper.registerAllowedMethod('stash', [String.class], null) - helper.registerAllowedMethod('git', [Map.class], {m -> - gitMap = m - }) - helper.registerAllowedMethod("findFiles", [Map.class], { map -> - def files - if(map.glob == 'notFound.json') - files = [] - else if(map.glob == '**/*.postman_collection.json') - files = [ - new File("testCollectionsFolder/A.postman_collection.json"), - new File("testCollectionsFolder/B.postman_collection.json") - ] - else - files = [new File(map.glob)] - return files.toArray() - }) - Utils.metaClass.echo = { def m -> } - } - - @After - public void tearDown() { - Utils.metaClass = null - } - - @Test - void testExecuteNewmanDefault() throws Exception { - stepRule.step.newmanExecute( - script: nullScript, - juStabUtils: utils, - newmanCollection: 'testCollection', - newmanEnvironment: 'testEnvironment', - newmanGlobals: 'testGlobals' - ) - // asserts - assertThat(shellRule.shell, hasItem(endsWith('npm install newman newman-reporter-html --global --quiet'))) - assertThat(shellRule.shell, hasItem(endsWith('newman run \'testCollection\' --environment \'testEnvironment\' --globals \'testGlobals\' --reporters junit,html --reporter-junit-export \'target/newman/TEST-testCollection.xml\' --reporter-html-export \'target/newman/TEST-testCollection.html\''))) - assertThat(dockerExecuteRule.dockerParams.dockerImage, is('node:lts-stretch')) - assertThat(loggingRule.log, containsString('[newmanExecute] Found files [testCollection]')) - assertJobStatusSuccess() - } - - @Test - void testDockerFromCustomStepConfiguration() { - - def expectedImage = 'image:test' - def expectedEnvVars = ['env1': 'value1', 'env2': 'value2'] - def expectedOptions = '--opt1=val1 --opt2=val2 --opt3' - def expectedWorkspace = '/path/to/workspace' - - nullScript.commonPipelineEnvironment.configuration = [steps:[newmanExecute:[ - dockerImage: expectedImage, - dockerOptions: expectedOptions, - dockerEnvVars: expectedEnvVars, - dockerWorkspace: expectedWorkspace - ]]] - - stepRule.step.newmanExecute( - script: nullScript, - juStabUtils: utils - ) - - assert expectedImage == dockerExecuteRule.dockerParams.dockerImage - assert expectedOptions == dockerExecuteRule.dockerParams.dockerOptions - assert expectedEnvVars.equals(dockerExecuteRule.dockerParams.dockerEnvVars) - assert expectedWorkspace == dockerExecuteRule.dockerParams.dockerWorkspace - } - - @Test - void testGlobalInstall() throws Exception { - stepRule.step.newmanExecute( - script: nullScript, - juStabUtils: utils, - newmanCollection: 'testCollection', - newmanEnvironment: 'testEnvironment', - newmanGlobals: 'testGlobals' - ) - // asserts - assertThat(shellRule.shell, hasItem(startsWith('NPM_CONFIG_PREFIX=~/.npm-global '))) - assertThat(shellRule.shell, hasItem(startsWith('PATH=$PATH:~/.npm-global/bin'))) - assertJobStatusSuccess() - } - - @Test - void testExecuteNewmanWithNoCollection() throws Exception { - thrown.expectMessage('[newmanExecute] No collection found with pattern \'notFound.json\'') - - stepRule.step.newmanExecute( - script: nullScript, - juStabUtils: utils, - newmanCollection: 'notFound.json' - ) - // asserts - assertJobStatusFailure() - } - - @Test - void testExecuteNewmanFailOnError() throws Exception { - stepRule.step.newmanExecute( - script: nullScript, - juStabUtils: utils, - newmanCollection: 'testCollection', - newmanEnvironment: 'testEnvironment', - newmanGlobals: 'testGlobals', - dockerImage: 'testImage', - testRepository: 'testRepo', - failOnError: false - ) - // asserts - assertThat(dockerExecuteRule.dockerParams.dockerImage, is('testImage')) - assertThat(gitMap.url, is('testRepo')) - assertThat(shellRule.shell, hasItem(endsWith('newman run \'testCollection\' --environment \'testEnvironment\' --globals \'testGlobals\' --reporters junit,html --reporter-junit-export \'target/newman/TEST-testCollection.xml\' --reporter-html-export \'target/newman/TEST-testCollection.html\' --suppress-exit-code'))) - assertJobStatusSuccess() - } - - @Test - void testExecuteNewmanWithFolder() throws Exception { - stepRule.step.newmanExecute( - script: nullScript, - juStabUtils: utils, - newmanRunCommand: 'run ${config.newmanCollection} --iteration-data testDataFile --reporters junit,html --reporter-junit-export target/newman/TEST-${config.newmanCollection.toString().replace(File.separatorChar,(char)\'_\').tokenize(\'.\').first()}.xml --reporter-html-export target/newman/TEST-${config.newmanCollection.toString().replace(File.separatorChar,(char)\'_\').tokenize(\'.\').first()}.html' - ) - // asserts - assertThat(shellRule.shell, hasItem(endsWith('newman run testCollectionsFolder'+File.separatorChar+'A.postman_collection.json --iteration-data testDataFile --reporters junit,html --reporter-junit-export target/newman/TEST-testCollectionsFolder_A.xml --reporter-html-export target/newman/TEST-testCollectionsFolder_A.html'))) - assertThat(shellRule.shell, hasItem(endsWith('newman run testCollectionsFolder'+File.separatorChar+'B.postman_collection.json --iteration-data testDataFile --reporters junit,html --reporter-junit-export target/newman/TEST-testCollectionsFolder_B.xml --reporter-html-export target/newman/TEST-testCollectionsFolder_B.html'))) - assertJobStatusSuccess() - } - - @Test - void testExecuteNewmanCfAppsWithSecrets() throws Exception { - def jsonResponse = '{ "system_env_json":{"VCAP_SERVICES":{"xsuaa":[{"credentials":{"clientid":"myclientid", "clientsecret":"myclientsecret"}}]}}, "authorization_endpoint": "myAuthEndPoint", "access_token": "myAccessToken", "resources":[{"guid":"myGuid", "links":{"self":{"href":"myAppUrl"}}}] }' - jenkinsCredentialsRule.withCredentials('credentialsId', 'myuser', 'topsecret') - helper.registerAllowedMethod('httpRequest', [Map.class] , { - return [content: jsonResponse, status: 200] - }) - helper.registerAllowedMethod('readJSON', [Map.class] , { - return new JsonSlurper().parseText(jsonResponse) - }) - - stepRule.step.newmanExecute( - script: nullScript, - juStabUtils: utils, - newmanCollection: 'testCollection', - newmanEnvironment: 'testEnvironment', - newmanGlobals: 'testGlobals', - dockerImage: 'testImage', - testRepository: 'testRepo', - failOnError: false, - cloudFoundry: ["apiEndpoint": "http://fake.endpoint.com", "org":"myOrg", "space": "mySpace", "credentialsId": "credentialsId"], - cfAppsWithSecrets: ['app1', 'app2'] - ) - // asserts - assertThat(shellRule.shell, hasItem(endsWith('--env-var app1_clientid=myclientid --env-var app1_clientsecret=myclientsecret --env-var app2_clientid=myclientid --env-var app2_clientsecret=myclientsecret'))) - assertJobStatusSuccess() - } -} diff --git a/vars/newmanExecute.groovy b/vars/newmanExecute.groovy index 15ad0f384..43d2b0e6e 100644 --- a/vars/newmanExecute.groovy +++ b/vars/newmanExecute.groovy @@ -1,32 +1,25 @@ +import com.sap.piper.ConfigurationHelper +import com.sap.piper.integration.CloudFoundry +import groovy.transform.Field +import com.sap.piper.GitUtils + import static com.sap.piper.Prerequisites.checkScript -import com.sap.piper.ConfigurationHelper -import com.sap.piper.GenerateDocumentation -import com.sap.piper.GitUtils -import com.sap.piper.Utils -import com.sap.piper.integration.CloudFoundry - -import groovy.text.GStringTemplateEngine -import groovy.transform.Field - @Field String STEP_NAME = getClass().getName() +@Field String METADATA_FILE = 'metadata/newmanExecute.yaml' -@Field Set GENERAL_CONFIG_KEYS = STEP_CONFIG_KEYS - -@Field Set STEP_CONFIG_KEYS = [ - /** @see dockerExecute */ - 'dockerImage', - /** @see dockerExecute*/ - 'dockerEnvVars', - /** @see dockerExecute */ - 'dockerOptions', - /** @see dockerExecute*/ - 'dockerWorkspace', +@Field Set CONFIG_KEYS = [ /** - * Defines the behavior, in case tests fail. - * @possibleValues `true`, `false` + * Define name array of cloud foundry apps deployed for which secrets (clientid and clientsecret) will be appended + * to the newman command that overrides the environment json entries + * (--env-var =${clientid} & --env-var =${clientsecret}) */ - 'failOnError', + "cfAppsWithSecrets", + /** + * Define an additional repository where the test implementation is located. + * For protected repositories the `testRepository` needs to contain the ssh git url. + */ + 'testRepository', /** * Only if `testRepository` is provided: Branch of testRepository, defaults to master. */ @@ -36,166 +29,41 @@ import groovy.transform.Field * @possibleValues Jenkins credentials id */ 'gitSshKeyCredentialsId', - /** - * The test collection that should be executed. This could also be a file pattern. - */ - 'newmanCollection', - /** - * Specify an environment file path or URL. Environments provide a set of variables that one can use within collections. - * see also [Newman docs](https://github.com/postmanlabs/newman#newman-run-collection-file-source-options) - */ - 'newmanEnvironment', - /** - * Specify the file path or URL for global variables. Global variables are similar to environment variables but have a lower precedence and can be overridden by environment variables having the same name. - * see also [Newman docs](https://github.com/postmanlabs/newman#newman-run-collection-file-source-options) - */ - 'newmanGlobals', - /** - * The shell command that will be executed inside the docker container to install Newman. - */ - 'newmanInstallCommand', - /** - * The newman command that will be executed inside the docker container. - */ - 'newmanRunCommand', - /** - * If specific stashes should be considered for the tests, you can pass this via this parameter. - */ - 'stashContent', - /** - * Define an additional repository where the test implementation is located. - * For protected repositories the `testRepository` needs to contain the ssh git url. - */ - 'testRepository', - /** - * Define name array of cloud foundry apps deployed for which secrets (clientid and clientsecret) will be appended - * to the newman command that overrides the environment json entries - * (--env-var =${clientid} & --env-var =${clientsecret}) - */ - 'cfAppsWithSecrets', - 'cloudFoundry', - /** - * Cloud Foundry API endpoint. - * @parentConfigKey cloudFoundry - */ - 'apiEndpoint', - /** - * Credentials to be used for deployment. - * @parentConfigKey cloudFoundry - */ - 'credentialsId', - /** - * Cloud Foundry target organization. - * @parentConfigKey cloudFoundry - */ - 'org', - /** - * Cloud Foundry target space. - * @parentConfigKey cloudFoundry - */ - 'space', - /** - * Print more detailed information into the log. - * @possibleValues `true`, `false` - */ - 'verbose' ] @Field Map CONFIG_KEY_COMPATIBILITY = [cloudFoundry: [apiEndpoint: 'cfApiEndpoint', credentialsId: 'cfCredentialsId', org: 'cfOrg', space: 'cfSpace']] -@Field Set PARAMETER_KEYS = STEP_CONFIG_KEYS - -/** - * This script executes [Postman](https://www.getpostman.com) tests from a collection via the [Newman](https://www.getpostman.com/docs/v6/postman/collection_runs/command_line_integration_with_newman) command line tool. - */ -@GenerateDocumentation void call(Map parameters = [:]) { - handlePipelineStepErrors(stepName: STEP_NAME, stepParameters: parameters) { + final script = checkScript(this, parameters) ?: this + String stageName = parameters.stageName ?: env.STAGE_NAME + Map config = ConfigurationHelper.newInstance(this) + .loadStepDefaults([:], stageName) + .mixinGeneralConfig(script.commonPipelineEnvironment, CONFIG_KEYS) + .mixinStepConfig(script.commonPipelineEnvironment, CONFIG_KEYS) + .mixinStageConfig(script.commonPipelineEnvironment, stageName, CONFIG_KEYS) + .mixin(parameters, CONFIG_KEYS, CONFIG_KEY_COMPATIBILITY) + .use() - def script = checkScript(this, parameters) ?: this - def utils = parameters?.juStabUtils ?: new Utils() - String stageName = parameters.stageName ?: env.STAGE_NAME + if (parameters.testRepository || config.testRepository ) { + parameters.stashContent = [GitUtils.handleTestRepository(this, [gitBranch: config.gitBranch, gitSshKeyCredentialsId: config.gitSshKeyCredentialsId, testRepository: config.testRepository])] + } - // load default & individual configuration - Map config = ConfigurationHelper.newInstance(this) - .loadStepDefaults([:], stageName) - .mixinGeneralConfig(script.commonPipelineEnvironment, GENERAL_CONFIG_KEYS) - .mixinStepConfig(script.commonPipelineEnvironment, STEP_CONFIG_KEYS) - .mixinStageConfig(script.commonPipelineEnvironment, stageName, STEP_CONFIG_KEYS) - .mixin(parameters, PARAMETER_KEYS, CONFIG_KEY_COMPATIBILITY) - .use() - - new Utils().pushToSWA([ - step: STEP_NAME, - stepParamKey1: 'scriptMissing', - stepParam1: parameters?.script == null - ], config) - - config.stashContent = config.testRepository - ?[GitUtils.handleTestRepository(this, config)] - :utils.unstashAll(config.stashContent) - - List collectionList = findFiles(glob: config.newmanCollection)?.toList() - if (collectionList.isEmpty()) { - error "[${STEP_NAME}] No collection found with pattern '${config.newmanCollection}'" - } else { - echo "[${STEP_NAME}] Found files ${collectionList}" - } - - dockerExecute( - script: script, - dockerImage: config.dockerImage, - dockerEnvVars: config.dockerEnvVars, - dockerOptions: config.dockerOptions, - dockerWorkspace: config.dockerWorkspace, - stashContent: config.stashContent - ) { - sh returnStatus: true, script: """ - node --version - npm --version - """ - sh "NPM_CONFIG_PREFIX=~/.npm-global ${config.newmanInstallCommand}" - for(String collection : collectionList){ - def collectionDisplayName = collection.toString().replace(File.separatorChar,(char)'_').tokenize('.').first() - // resolve templates - def command = GStringTemplateEngine.newInstance() - .createTemplate(config.newmanRunCommand) - .make([ - config: config.plus([newmanCollection: collection]), - collectionDisplayName: collectionDisplayName - ]).toString() - def command_secrets = '' - if(config.cfAppsWithSecrets){ - CloudFoundry cfUtils = new CloudFoundry(script); - config.cfAppsWithSecrets.each { appName -> - def xsuaaCredentials = cfUtils.getXsuaaCredentials(config.cloudFoundry.apiEndpoint, - config.cloudFoundry.org, - config.cloudFoundry.space, - config.cloudFoundry.credentialsId, - appName, - config.verbose ? true : false ) //to avoid config.verbose as "null" if undefined in yaml and since function parameter boolean - command_secrets += " --env-var ${appName}_clientid=${xsuaaCredentials.clientid} --env-var ${appName}_clientsecret=${xsuaaCredentials.clientsecret}" - echo "Exposing client id and secret for ${appName}: as ${appName}_clientid and ${appName}_clientsecret to newman" - } - } - - if(!config.failOnError) command += ' --suppress-exit-code' - try { - if (config.cfAppsWithSecrets && command_secrets){ - echo "PATH=\$PATH:~/.npm-global/bin newman ${command} **env/secrets**" - sh """ - set +x - PATH=\$PATH:~/.npm-global/bin newman ${command} ${command_secrets} - """ - } - else{ - sh "PATH=\$PATH:~/.npm-global/bin newman ${command}" - } - } - catch (e) { - error "[${STEP_NAME}] ERROR: The execution of the newman tests failed, see the log for details." - } - } + List cfCredentials = [] + if (config.cfAppsWithSecrets) { + CloudFoundry cfUtils = new CloudFoundry(script); + config.cfAppsWithSecrets.each { appName -> + def xsuaaCredentials = cfUtils.getXsuaaCredentials(config.cloudFoundry.apiEndpoint, + config.cloudFoundry.org, + config.cloudFoundry.space, + config.cloudFoundry.credentialsId, + appName, + config.verbose ? true : false ) + cfCredentials.add("PIPER_NEWMANEXECUTE_${appName}_clientid=${xsuaaCredentials.clientid}") + cfCredentials.add("PIPER_NEWMANEXECUTE_${appName}_clientsecret=${xsuaaCredentials.clientsecret}") + echo "Exposing client id and secret for ${appName}: as ${appName}_clientid and ${appName}_clientsecret to newmanExecute" } } + withEnv(cfCredentials) { + piperExecuteBin(parameters, STEP_NAME, METADATA_FILE, []) + } } diff --git a/vars/uiVeri5ExecuteTests.groovy b/vars/uiVeri5ExecuteTests.groovy index 48e5c3dd4..f7b41f653 100644 --- a/vars/uiVeri5ExecuteTests.groovy +++ b/vars/uiVeri5ExecuteTests.groovy @@ -33,7 +33,7 @@ void call(Map parameters = [:]) { .use() if (parameters.testRepository || config.testRepository ) { - parameters.stashContent = GitUtils.handleTestRepository(this, [gitBranch: config.gitBranch, gitSshKeyCredentialsId: config.gitSshKeyCredentialsId, testRepository: config.testRepository]) + parameters.stashContent = [GitUtils.handleTestRepository(this, [gitBranch: config.gitBranch, gitSshKeyCredentialsId: config.gitSshKeyCredentialsId, testRepository: config.testRepository])] } List credentials = [