From e90548d41d2f1edf3fd3f5744eca277a015b4b69 Mon Sep 17 00:00:00 2001 From: Marcus Holl Date: Fri, 12 Feb 2021 08:50:38 +0100 Subject: [PATCH] Introduce checkChangeInDevelopment (#2504) * Introduce checkChangeInDevelopment --- cmd/checkChangeInDevelopment.go | 109 ++++++++++++ cmd/checkChangeInDevelopment_generated.go | 161 ++++++++++++++++++ ...checkChangeInDevelopment_generated_test.go | 17 ++ cmd/checkChangeInDevelopment_test.go | 122 +++++++++++++ cmd/metadata_generated.go | 1 + cmd/piper.go | 1 + .../metadata/checkChangeInDevelopmemt.yaml | 61 +++++++ 7 files changed, 472 insertions(+) create mode 100644 cmd/checkChangeInDevelopment.go create mode 100644 cmd/checkChangeInDevelopment_generated.go create mode 100644 cmd/checkChangeInDevelopment_generated_test.go create mode 100644 cmd/checkChangeInDevelopment_test.go create mode 100644 resources/metadata/checkChangeInDevelopmemt.yaml diff --git a/cmd/checkChangeInDevelopment.go b/cmd/checkChangeInDevelopment.go new file mode 100644 index 000000000..d8f4856fd --- /dev/null +++ b/cmd/checkChangeInDevelopment.go @@ -0,0 +1,109 @@ +package cmd + +import ( + "fmt" + "github.com/SAP/jenkins-library/pkg/command" + "github.com/SAP/jenkins-library/pkg/log" + "github.com/SAP/jenkins-library/pkg/telemetry" + "github.com/pkg/errors" + "strings" +) + +type checkChangeInDevelopmentUtils interface { + command.ExecRunner + GetExitCode() int + + // Add more methods here, or embed additional interfaces, or remove/replace as required. + // The checkChangeInDevelopmentUtils interface should be descriptive of your runtime dependencies, + // i.e. include everything you need to be able to mock in tests. + // Unit tests shall be executable in parallel (not depend on global state), and don't (re-)test dependencies. +} + +type checkChangeInDevelopmentUtilsBundle struct { + *command.Command + + // Embed more structs as necessary to implement methods or interfaces you add to checkChangeInDevelopmentUtils. + // Structs embedded in this way must each have a unique set of methods attached. + // If there is no struct which implements the method you need, attach the method to + // checkChangeInDevelopmentUtilsBundle and forward to the implementation of the dependency. +} + +func newCheckChangeInDevelopmentUtils() checkChangeInDevelopmentUtils { + utils := checkChangeInDevelopmentUtilsBundle{ + Command: &command.Command{}, + } + // Reroute command output to logging framework + utils.Stdout(log.Writer()) + utils.Stderr(log.Writer()) + return &utils +} + +func checkChangeInDevelopment(config checkChangeInDevelopmentOptions, telemetryData *telemetry.CustomData) { + // Utils can be used wherever the command.ExecRunner interface is expected. + // It can also be used for example as a mavenExecRunner. + utils := newCheckChangeInDevelopmentUtils() + + // For HTTP calls import piperhttp "github.com/SAP/jenkins-library/pkg/http" + // and use a &piperhttp.Client{} in a custom system + // Example: step checkmarxExecuteScan.go + + // Error situations should be bubbled up until they reach the line below which will then stop execution + // through the log.Entry().Fatal() call leading to an os.Exit(1) in the end. + err := runCheckChangeInDevelopment(&config, telemetryData, utils) + if err != nil { + log.Entry().WithError(err).Fatal("step execution failed") + } +} + +func runCheckChangeInDevelopment(config *checkChangeInDevelopmentOptions, telemetryData *telemetry.CustomData, utils checkChangeInDevelopmentUtils) error { + + log.Entry().Infof("Checking change status for change '%s'", config.ChangeDocumentID) + + isInDevelopment, err := isChangeInDevelopment(config, utils) + if err != nil { + return err + } + + if isInDevelopment { + log.Entry().Infof("Change '%s' is in status 'in development'.", config.ChangeDocumentID) + return nil + } + if config.FailIfStatusIsNotInDevelopment { + return fmt.Errorf("Change '%s' is not in status 'in development'", config.ChangeDocumentID) + } + log.Entry().Warningf("Change '%s' is not in status 'in development'. Failing the step has been explicitly disabled.", config.ChangeDocumentID) + return nil +} + +func isChangeInDevelopment(config *checkChangeInDevelopmentOptions, utils checkChangeInDevelopmentUtils) (bool, error) { + + if len(config.ClientOpts) > 0 { + utils.AppendEnv([]string{fmt.Sprintf("CMCLIENT_OPTS=%s", strings.Join(config.ClientOpts, " "))}) + } + + err := utils.RunExecutable("cmclient", + "--endpoint", config.Endpoint, + "--user", config.Username, + "--password", config.Password, + "--backend-type", "SOLMAN", + "is-change-in-development", + "--change-id", config.ChangeDocumentID, + "--return-code") + + if err != nil { + return false, errors.Wrap(err, "Cannot retrieve change status") + } + + exitCode := utils.GetExitCode() + + hint := "Check log for details" + if exitCode == 0 { + return true, nil + } else if exitCode == 3 { + return false, nil + } else if exitCode == 2 { + hint = "Invalid credentials" + } + + return false, fmt.Errorf("Cannot retrieve change status: %s", hint) +} diff --git a/cmd/checkChangeInDevelopment_generated.go b/cmd/checkChangeInDevelopment_generated.go new file mode 100644 index 000000000..c487bb68b --- /dev/null +++ b/cmd/checkChangeInDevelopment_generated.go @@ -0,0 +1,161 @@ +// 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 checkChangeInDevelopmentOptions struct { + Endpoint string `json:"endpoint,omitempty"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + ChangeDocumentID string `json:"changeDocumentId,omitempty"` + FailIfStatusIsNotInDevelopment bool `json:"failIfStatusIsNotInDevelopment,omitempty"` + ClientOpts []string `json:"clientOpts,omitempty"` +} + +// CheckChangeInDevelopmentCommand Checks if a certain change is in status 'in development' +func CheckChangeInDevelopmentCommand() *cobra.Command { + const STEP_NAME = "checkChangeInDevelopment" + + metadata := checkChangeInDevelopmentMetadata() + var stepConfig checkChangeInDevelopmentOptions + var startTime time.Time + + var createCheckChangeInDevelopmentCmd = &cobra.Command{ + Use: STEP_NAME, + Short: "Checks if a certain change is in status 'in development'", + Long: `"Checks if a certain change is in status 'in development'"`, + 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 + } + log.RegisterSecret(stepConfig.Username) + log.RegisterSecret(stepConfig.Password) + + 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) + checkChangeInDevelopment(stepConfig, &telemetryData) + telemetryData.ErrorCode = "0" + log.Entry().Info("SUCCESS") + }, + } + + addCheckChangeInDevelopmentFlags(createCheckChangeInDevelopmentCmd, &stepConfig) + return createCheckChangeInDevelopmentCmd +} + +func addCheckChangeInDevelopmentFlags(cmd *cobra.Command, stepConfig *checkChangeInDevelopmentOptions) { + cmd.Flags().StringVar(&stepConfig.Endpoint, "endpoint", os.Getenv("PIPER_endpoint"), "The service endpoint") + cmd.Flags().StringVar(&stepConfig.Username, "username", os.Getenv("PIPER_username"), "The user") + cmd.Flags().StringVar(&stepConfig.Password, "password", os.Getenv("PIPER_password"), "The password") + cmd.Flags().StringVar(&stepConfig.ChangeDocumentID, "changeDocumentId", os.Getenv("PIPER_changeDocumentId"), "The change document which should be checked for the status") + cmd.Flags().BoolVar(&stepConfig.FailIfStatusIsNotInDevelopment, "failIfStatusIsNotInDevelopment", true, "lets the build fail in case the change is not in status 'in developent'. Otherwise a warning is emitted to the log") + cmd.Flags().StringSliceVar(&stepConfig.ClientOpts, "clientOpts", []string{}, "additional options passed to cm client, e.g. for troubleshooting") + + cmd.MarkFlagRequired("endpoint") + cmd.MarkFlagRequired("username") + cmd.MarkFlagRequired("password") + cmd.MarkFlagRequired("changeDocumentId") +} + +// retrieve step metadata +func checkChangeInDevelopmentMetadata() config.StepData { + var theMetaData = config.StepData{ + Metadata: config.StepMetadata{ + Name: "checkChangeInDevelopment", + Aliases: []config.Alias{}, + Description: "Checks if a certain change is in status 'in development'", + }, + Spec: config.StepSpec{ + Inputs: config.StepInputs{ + Parameters: []config.StepParameters{ + { + Name: "endpoint", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS", "GENERAL"}, + Type: "string", + Mandatory: true, + Aliases: []config.Alias{{Name: "changeManagement/endpoint"}}, + }, + { + Name: "username", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS", "GENERAL"}, + Type: "string", + Mandatory: true, + Aliases: []config.Alias{}, + }, + { + Name: "password", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "GENERAL"}, + Type: "string", + Mandatory: true, + Aliases: []config.Alias{}, + }, + { + Name: "changeDocumentId", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: true, + Aliases: []config.Alias{}, + }, + { + Name: "failIfStatusIsNotInDevelopment", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "bool", + Mandatory: false, + Aliases: []config.Alias{}, + }, + { + Name: "clientOpts", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "[]string", + Mandatory: false, + Aliases: []config.Alias{}, + }, + }, + }, + }, + } + return theMetaData +} diff --git a/cmd/checkChangeInDevelopment_generated_test.go b/cmd/checkChangeInDevelopment_generated_test.go new file mode 100644 index 000000000..ee4657087 --- /dev/null +++ b/cmd/checkChangeInDevelopment_generated_test.go @@ -0,0 +1,17 @@ +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCheckChangeInDevelopmentCommand(t *testing.T) { + t.Parallel() + + testCmd := CheckChangeInDevelopmentCommand() + + // only high level testing performed - details are tested in step generation procedure + assert.Equal(t, "checkChangeInDevelopment", testCmd.Use, "command name incorrect") + +} diff --git a/cmd/checkChangeInDevelopment_test.go b/cmd/checkChangeInDevelopment_test.go new file mode 100644 index 000000000..82fadc5ba --- /dev/null +++ b/cmd/checkChangeInDevelopment_test.go @@ -0,0 +1,122 @@ +package cmd + +import ( + "fmt" + "github.com/SAP/jenkins-library/pkg/mock" + "github.com/stretchr/testify/assert" + "testing" +) + +type checkChangeInDevelopmentMockUtils struct { + *mock.ExecMockRunner +} + +func newCheckChangeInDevelopmentTestsUtils() checkChangeInDevelopmentMockUtils { + utils := checkChangeInDevelopmentMockUtils{ + ExecMockRunner: &mock.ExecMockRunner{}, + } + return utils +} + +func TestRunCheckChangeInDevelopment(t *testing.T) { + + t.Parallel() + + config := checkChangeInDevelopmentOptions{ + Endpoint: "https://example.org/cm", + Username: "me", + Password: "****", + ChangeDocumentID: "12345678", + ClientOpts: []string{"-Dabc=123", "-Ddef=456"}, + FailIfStatusIsNotInDevelopment: true, // this is the default + } + + expectedShellCall := mock.ExecCall{ + Exec: "cmclient", + Params: []string{ + "--endpoint", "https://example.org/cm", + "--user", "me", + "--password", "****", + "--backend-type", "SOLMAN", + "is-change-in-development", + "--change-id", "12345678", + "--return-code", + }, + } + + t.Run("change found and in status IN_DEVELOPMENT", func(t *testing.T) { + + cmd := newCheckChangeInDevelopmentTestsUtils() + cmd.ExitCode = 0 // this exit code represents a change in status IN_DEVELOPMENT + + err := runCheckChangeInDevelopment(&config, nil, cmd) + + if assert.NoError(t, err) { + assert.Equal(t, []string{"CMCLIENT_OPTS=-Dabc=123 -Ddef=456"}, cmd.Env) + assert.Equal(t, []mock.ExecCall{expectedShellCall}, cmd.Calls) + } + }) + + t.Run("change found and not in status IN_DEVELOPMENT", func(t *testing.T) { + + cmd := newCheckChangeInDevelopmentTestsUtils() + cmd.ExitCode = 3 // this exit code represents a change which is not in status IN_DEVELOPMENT + + err := runCheckChangeInDevelopment(&config, nil, cmd) + + if assert.EqualError(t, err, "Change '12345678' is not in status 'in development'") { + assert.Equal(t, []mock.ExecCall{expectedShellCall}, cmd.Calls) + } + }) + + t.Run("change found and not in status IN_DEVELOPMENT, but we don't fail", func(t *testing.T) { + + cmd := newCheckChangeInDevelopmentTestsUtils() + cmd.ExitCode = 3 // this exit code represents a change which is not in status IN_DEVELOPMENT + + myConfig := config + myConfig.FailIfStatusIsNotInDevelopment = false // needs to be explicitly configured + + err := runCheckChangeInDevelopment(&myConfig, nil, cmd) + + if assert.NoError(t, err) { + assert.Equal(t, []mock.ExecCall{expectedShellCall}, cmd.Calls) + } + }) + + t.Run("invalid credentials", func(t *testing.T) { + + cmd := newCheckChangeInDevelopmentTestsUtils() + cmd.ExitCode = 2 // this exit code represents invalid credentials + + err := runCheckChangeInDevelopment(&config, nil, cmd) + + if assert.EqualError(t, err, "Cannot retrieve change status: Invalid credentials") { + assert.Equal(t, []mock.ExecCall{expectedShellCall}, cmd.Calls) + } + }) + + t.Run("generic failure reported via exit code", func(t *testing.T) { + + cmd := newCheckChangeInDevelopmentTestsUtils() + cmd.ExitCode = 1 // this exit code indicates something went wrong + + err := runCheckChangeInDevelopment(&config, nil, cmd) + + if assert.EqualError(t, err, "Cannot retrieve change status: Check log for details") { + assert.Equal(t, []mock.ExecCall{expectedShellCall}, cmd.Calls) + } + }) + + t.Run("generic failure reported via error", func(t *testing.T) { + + cmd := newCheckChangeInDevelopmentTestsUtils() + cmd.ShouldFailOnCommand = map[string]error{"cm.*": fmt.Errorf("%v", "Something went wrong")} + + err := runCheckChangeInDevelopment(&config, nil, cmd) + + if assert.EqualError(t, err, "Cannot retrieve change status: Something went wrong") { + assert.Equal(t, []mock.ExecCall{expectedShellCall}, cmd.Calls) + } + }) +} diff --git a/cmd/metadata_generated.go b/cmd/metadata_generated.go index 7e22340df..6e8a0d445 100644 --- a/cmd/metadata_generated.go +++ b/cmd/metadata_generated.go @@ -21,6 +21,7 @@ func GetAllStepMetadata() map[string]config.StepData { "abapEnvironmentCreateSystem": abapEnvironmentCreateSystemMetadata(), "abapEnvironmentPullGitRepo": abapEnvironmentPullGitRepoMetadata(), "abapEnvironmentRunATCCheck": abapEnvironmentRunATCCheckMetadata(), + "checkChangeInDevelopment": checkChangeInDevelopmentMetadata(), "checkmarxExecuteScan": checkmarxExecuteScanMetadata(), "cloudFoundryCreateService": cloudFoundryCreateServiceMetadata(), "cloudFoundryCreateServiceKey": cloudFoundryCreateServiceKeyMetadata(), diff --git a/cmd/piper.go b/cmd/piper.go index 642bf2baa..9ec0dd9d0 100644 --- a/cmd/piper.go +++ b/cmd/piper.go @@ -121,6 +121,7 @@ func Execute() { rootCmd.AddCommand(CloudFoundryCreateSpaceCommand()) rootCmd.AddCommand(CloudFoundryDeleteSpaceCommand()) rootCmd.AddCommand(VaultRotateSecretIdCommand()) + rootCmd.AddCommand(CheckChangeInDevelopmentCommand()) rootCmd.AddCommand(TransportRequestUploadCTSCommand()) rootCmd.AddCommand(IntegrationArtifactDeployCommand()) rootCmd.AddCommand(IntegrationArtifactUpdateConfigurationCommand()) diff --git a/resources/metadata/checkChangeInDevelopmemt.yaml b/resources/metadata/checkChangeInDevelopmemt.yaml new file mode 100644 index 000000000..46d2a56cd --- /dev/null +++ b/resources/metadata/checkChangeInDevelopmemt.yaml @@ -0,0 +1,61 @@ +metadata: + name: checkChangeInDevelopment + description: "Checks if a certain change is in status 'in development'" + longDescription: | + "Checks if a certain change is in status 'in development'" + +spec: + inputs: + params: + - name: endpoint + type: string + mandatory: true + description: "The service endpoint" + aliases: + - name: changeManagement/endpoint + scope: + - PARAMETERS + - STAGES + - STEPS + - GENERAL + - name: username + type: string + mandatory: true + description: "The user" + secret: true + scope: + - PARAMETERS + - STAGES + - STEPS + - GENERAL + - name: password + type: string + mandatory: true + description: "The password" + secret: true + scope: + - PARAMETERS + - GENERAL + - name: changeDocumentId + type: string + mandatory: true + description: "The change document which should be checked for the status" + scope: + - PARAMETERS + - STAGES + - STEPS + - name: failIfStatusIsNotInDevelopment + type: bool + default: true + description: "lets the build fail in case the change is not in status 'in developent'. Otherwise a warning is emitted to the log" + scope: + - PARAMETERS + - STAGES + - STEPS + - name: clientOpts + type: "[]string" + description: "additional options passed to cm client, e.g. for troubleshooting" + scope: + - PARAMETERS + - STAGES + - STEPS