diff --git a/cmd/gctsDeploy.go b/cmd/gctsDeploy.go new file mode 100644 index 000000000..caf681e86 --- /dev/null +++ b/cmd/gctsDeploy.go @@ -0,0 +1,89 @@ +package cmd + +import ( + "io/ioutil" + "net/http/cookiejar" + "net/url" + + "github.com/Jeffail/gabs/v2" + "github.com/SAP/jenkins-library/pkg/command" + piperhttp "github.com/SAP/jenkins-library/pkg/http" + "github.com/SAP/jenkins-library/pkg/log" + "github.com/SAP/jenkins-library/pkg/telemetry" + "github.com/pkg/errors" +) + +func gctsDeploy(config gctsDeployOptions, telemetryData *telemetry.CustomData) { + // for command execution use Command + c := command.Command{} + // reroute command output to logging framework + c.Stdout(log.Entry().Writer()) + c.Stderr(log.Entry().Writer()) + + // 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 + httpClient := &piperhttp.Client{} + + // error situations should stop execution through log.Entry().Fatal() call which leads to an os.Exit(1) in the end + err := deployCommit(&config, telemetryData, &c, httpClient) + if err != nil { + log.Entry().WithError(err).Fatal("step execution failed") + } +} + +func deployCommit(config *gctsDeployOptions, telemetryData *telemetry.CustomData, command execRunner, httpClient piperhttp.Sender) error { + + cookieJar, cookieErr := cookiejar.New(nil) + if cookieErr != nil { + return errors.Wrap(cookieErr, "creating a cookie jar failed") + } + clientOptions := piperhttp.ClientOptions{ + CookieJar: cookieJar, + Username: config.Username, + Password: config.Password, + } + httpClient.SetOptions(clientOptions) + + requestURL := config.Host + + "/sap/bc/cts_abapvcs/repository/" + config.Repository + + "/pullByCommit?sap-client=" + config.Client + + if config.Commit != "" { + log.Entry().Infof("preparing to deploy specified commit %v", config.Commit) + params := url.Values{} + params.Add("request", config.Commit) + requestURL = requestURL + "&" + params.Encode() + } + + resp, httpErr := httpClient.SendRequest("GET", requestURL, nil, nil, nil) + + defer func() { + if resp != nil && resp.Body != nil { + resp.Body.Close() + } + }() + + if httpErr != nil { + return httpErr + } else if resp == nil { + return errors.New("did not retrieve a HTTP response") + } + + bodyText, readErr := ioutil.ReadAll(resp.Body) + + if readErr != nil { + return errors.Wrapf(readErr, "HTTP response body could not be read") + } + + response, parsingErr := gabs.ParseJSON([]byte(bodyText)) + + if parsingErr != nil { + return errors.Wrapf(parsingErr, "HTTP response body could not be parsed as JSON: %v", string(bodyText)) + } + + log.Entry(). + WithField("repository", config.Repository). + Infof("successfully deployed commit %v (previous commit was %v)", response.Path("toCommit").Data().(string), response.Path("fromCommit").Data().(string)) + return nil +} diff --git a/cmd/gctsDeploy_generated.go b/cmd/gctsDeploy_generated.go new file mode 100644 index 000000000..0f8db59df --- /dev/null +++ b/cmd/gctsDeploy_generated.go @@ -0,0 +1,157 @@ +// 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 gctsDeployOptions struct { + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + Repository string `json:"repository,omitempty"` + Host string `json:"host,omitempty"` + Client string `json:"client,omitempty"` + Commit string `json:"commit,omitempty"` +} + +// GctsDeployCommand Pulls a commit from the remote Git repository to a local repository +func GctsDeployCommand() *cobra.Command { + const STEP_NAME = "gctsDeploy" + + metadata := gctsDeployMetadata() + var stepConfig gctsDeployOptions + var startTime time.Time + + var createGctsDeployCmd = &cobra.Command{ + Use: STEP_NAME, + Short: "Pulls a commit from the remote Git repository to a local repository", + Long: `Pulls a commit from the corresponding remote Git repository to a specified local repository on an ABAP system. If no parameter is specified, this step will pull the latest commit available on the remote repository.`, + PreRunE: func(cmd *cobra.Command, args []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 { + 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(cmd *cobra.Command, args []string) { + telemetryData := telemetry.CustomData{} + telemetryData.ErrorCode = "1" + handler := func() { + telemetryData.Duration = fmt.Sprintf("%v", time.Since(startTime).Milliseconds()) + telemetry.Send(&telemetryData) + } + log.DeferExitHandler(handler) + defer handler() + telemetry.Initialize(GeneralConfig.NoTelemetry, STEP_NAME) + gctsDeploy(stepConfig, &telemetryData) + telemetryData.ErrorCode = "0" + }, + } + + addGctsDeployFlags(createGctsDeployCmd, &stepConfig) + return createGctsDeployCmd +} + +func addGctsDeployFlags(cmd *cobra.Command, stepConfig *gctsDeployOptions) { + cmd.Flags().StringVar(&stepConfig.Username, "username", os.Getenv("PIPER_username"), "User to authenticate to the ABAP system") + cmd.Flags().StringVar(&stepConfig.Password, "password", os.Getenv("PIPER_password"), "Password to authenticate to the ABAP system") + cmd.Flags().StringVar(&stepConfig.Repository, "repository", os.Getenv("PIPER_repository"), "Specifies the name (ID) of the local repsitory on the ABAP system") + cmd.Flags().StringVar(&stepConfig.Host, "host", os.Getenv("PIPER_host"), "Specifies the protocol and host adress, including the port. Please provide in the format '://:'") + cmd.Flags().StringVar(&stepConfig.Client, "client", os.Getenv("PIPER_client"), "Specifies the client of the ABAP system to be adressed") + cmd.Flags().StringVar(&stepConfig.Commit, "commit", os.Getenv("PIPER_commit"), "Specifies the commit to be deployed") + + cmd.MarkFlagRequired("username") + cmd.MarkFlagRequired("password") + cmd.MarkFlagRequired("repository") + cmd.MarkFlagRequired("host") + cmd.MarkFlagRequired("client") +} + +// retrieve step metadata +func gctsDeployMetadata() config.StepData { + var theMetaData = config.StepData{ + Metadata: config.StepMetadata{ + Name: "gctsDeploy", + Aliases: []config.Alias{}, + }, + Spec: config.StepSpec{ + Inputs: config.StepInputs{ + Parameters: []config.StepParameters{ + { + Name: "username", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: true, + Aliases: []config.Alias{}, + }, + { + Name: "password", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: true, + Aliases: []config.Alias{}, + }, + { + Name: "repository", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: true, + Aliases: []config.Alias{}, + }, + { + Name: "host", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: true, + Aliases: []config.Alias{}, + }, + { + Name: "client", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: true, + Aliases: []config.Alias{}, + }, + { + Name: "commit", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: false, + Aliases: []config.Alias{}, + }, + }, + }, + }, + } + return theMetaData +} diff --git a/cmd/gctsDeploy_generated_test.go b/cmd/gctsDeploy_generated_test.go new file mode 100644 index 000000000..5fd8566ec --- /dev/null +++ b/cmd/gctsDeploy_generated_test.go @@ -0,0 +1,16 @@ +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGctsDeployCommand(t *testing.T) { + + testCmd := GctsDeployCommand() + + // only high level testing performed - details are tested in step generation procudure + assert.Equal(t, "gctsDeploy", testCmd.Use, "command name incorrect") + +} diff --git a/cmd/gctsDeploy_test.go b/cmd/gctsDeploy_test.go new file mode 100644 index 000000000..0bfda9e05 --- /dev/null +++ b/cmd/gctsDeploy_test.go @@ -0,0 +1,110 @@ +package cmd + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestGctsDeploySuccess(t *testing.T) { + + config := gctsDeployOptions{ + Host: "http://testHost.com:50000", + Client: "000", + Repository: "testRepo", + Username: "testUser", + Password: "testPassword", + } + + t.Run("deploy latest commit", func(t *testing.T) { + + httpClient := httpMockGcts{StatusCode: 200, ResponseBody: `{ + "trkorr": "SIDK1234567", + "fromCommit": "f1cdb6a032c1d8187c0990b51e94e8d8bb9898b2", + "toCommit": "f1cdb6a032c1d8187c0990b51e94e8d8bb9898b2", + "log": [ + { + "time": 20180606130524, + "user": "JENKINS", + "section": "REPOSITORY_FACTORY", + "action": "CREATE_REPOSITORY", + "severity": "INFO", + "message": "Start action CREATE_REPOSITORY review", + "code": "GCTS.API.410" + } + ] + }`} + + err := deployCommit(&config, nil, nil, &httpClient) + + if assert.NoError(t, err) { + + t.Run("check url", func(t *testing.T) { + assert.Equal(t, "http://testHost.com:50000/sap/bc/cts_abapvcs/repository/testRepo/pullByCommit?sap-client=000", httpClient.URL) + }) + + t.Run("check method", func(t *testing.T) { + assert.Equal(t, "GET", httpClient.Method) + }) + + t.Run("check user", func(t *testing.T) { + assert.Equal(t, "testUser", httpClient.Options.Username) + }) + + t.Run("check password", func(t *testing.T) { + assert.Equal(t, "testPassword", httpClient.Options.Password) + }) + + } + + }) +} + +func TestGctsDeployFailure(t *testing.T) { + + config := gctsDeployOptions{ + Host: "http://testHost.com:50000", + Client: "000", + Repository: "testRepo", + Username: "testUser", + Password: "testPassword", + } + + t.Run("http error occurred", func(t *testing.T) { + + httpClient := httpMockGcts{StatusCode: 500, ResponseBody: `{ + "log": [ + { + "time": 20180606130524, + "user": "JENKINS", + "section": "REPOSITORY_FACTORY", + "action": "CREATE_REPOSITORY", + "severity": "INFO", + "message": "Start action CREATE_REPOSITORY review", + "code": "GCTS.API.410" + } + ], + "errorLog": [ + { + "time": 20180606130524, + "user": "JENKINS", + "section": "REPOSITORY_FACTORY", + "action": "CREATE_REPOSITORY", + "severity": "INFO", + "message": "Start action CREATE_REPOSITORY review", + "code": "GCTS.API.410" + } + ], + "exception": { + "message": "repository_not_found", + "description": "Repository not found", + "code": 404 + } + }`} + + err := deployCommit(&config, nil, nil, &httpClient) + + assert.EqualError(t, err, "a http error occurred") + + }) + +} diff --git a/cmd/piper.go b/cmd/piper.go index 156f8d243..47bba1868 100644 --- a/cmd/piper.go +++ b/cmd/piper.go @@ -84,6 +84,7 @@ func Execute() { rootCmd.AddCommand(AbapEnvironmentRunATCCheckCommand()) rootCmd.AddCommand(NpmExecuteScriptsCommand()) rootCmd.AddCommand(GctsCreateRepositoryCommand()) + rootCmd.AddCommand(GctsDeployCommand()) rootCmd.AddCommand(MalwareExecuteScanCommand()) addRootFlags(rootCmd) diff --git a/documentation/docs/steps/gctsDeployCommit.md b/documentation/docs/steps/gctsDeployCommit.md new file mode 100644 index 000000000..5144c50e9 --- /dev/null +++ b/documentation/docs/steps/gctsDeployCommit.md @@ -0,0 +1,41 @@ +# ${docGenStepName} + +## ${docGenDescription} + +## Prerequisites + +With this step you can deploy a commit from a remote Git repository to a local repository on an ABAP server. If no `commit` parameter is specified, this step will pull the latest commit available on the remote repository. +Learn more about the SAP git-enabled Central Transport Sytem (gCTS) [here](https://help.sap.com/viewer/4a368c163b08418890a406d413933ba7/201909.001/en-US/f319b168e87e42149e25e13c08d002b9.html). With gCTS, ABAP developments on ABAP servers can be maintained in Git repositories. + +## ${docGenParameters} + +## ${docGenConfiguration} + +## ${docJenkinsPluginDependencies} + +## Example + +Example configuration for the use in a `Jenkinsfile`. + +```groovy +gctsDeploy( + script: this, + host: "https://abap.server.com:port", + client: "000", + abapCredentialsId: 'ABAPUserPasswordCredentialsId', + repository: "myrepo" +) +``` + +Example for the use in a YAML configuration file (such as `.pipeline/config.yaml`). + +```yaml +steps: + <...> + gctsDeploy: + host: "https://abap.server.com:port" + client: "000" + username: "ABAPUsername" + password: "ABAPPassword" + repository: "myrepo" +``` diff --git a/resources/metadata/gctsDeployCommit.yaml b/resources/metadata/gctsDeployCommit.yaml new file mode 100644 index 000000000..82827117e --- /dev/null +++ b/resources/metadata/gctsDeployCommit.yaml @@ -0,0 +1,62 @@ +metadata: + name: gctsDeploy + description: Pulls a commit from the remote Git repository to a local repository + longDescription: | + Pulls a commit from the corresponding remote Git repository to a specified local repository on an ABAP system. If no parameter is specified, this step will pull the latest commit available on the remote repository. + +spec: + inputs: + secrets: + - name: abapCredentialsId + description: Jenkins credentials ID containing username and password for authentication to the ABAP system on which you want to deploy a commit + type: jenkins + params: + - name: username + type: string + description: User to authenticate to the ABAP system + scope: + - PARAMETERS + - STAGES + - STEPS + mandatory: true + secret: true + - name: password + type: string + description: Password to authenticate to the ABAP system + scope: + - PARAMETERS + - STAGES + - STEPS + mandatory: true + secret: true + - name: repository + type: string + description: Specifies the name (ID) of the local repsitory on the ABAP system + scope: + - PARAMETERS + - STAGES + - STEPS + mandatory: true + - name: host + type: string + description: Specifies the protocol and host adress, including the port. Please provide in the format '://:' + scope: + - PARAMETERS + - STAGES + - STEPS + mandatory: true + - name: client + type: string + description: Specifies the client of the ABAP system to be adressed + scope: + - PARAMETERS + - STAGES + - STEPS + mandatory: true + - name: commit + type: string + description: Specifies the commit to be deployed + scope: + - PARAMETERS + - STAGES + - STEPS diff --git a/test/groovy/CommonStepsTest.groovy b/test/groovy/CommonStepsTest.groovy index b33dd6f1c..c7a3f7171 100644 --- a/test/groovy/CommonStepsTest.groovy +++ b/test/groovy/CommonStepsTest.groovy @@ -135,6 +135,7 @@ public class CommonStepsTest extends BasePiperTest{ 'abapEnvironmentRunATCCheck', //implementing new golang pattern without fields 'sonarExecuteScan', //implementing new golang pattern without fields 'gctsCreateRepository', //implementing new golang pattern without fields + 'gctsDeploy', //implementing new golang pattern without fields ] @Test diff --git a/vars/gctsDeploy.groovy b/vars/gctsDeploy.groovy new file mode 100644 index 000000000..5179dd1ff --- /dev/null +++ b/vars/gctsDeploy.groovy @@ -0,0 +1,12 @@ +import groovy.transform.Field + + +@Field String STEP_NAME = getClass().getName() +@Field String METADATA_FILE = 'metadata/gctsDeploy.yaml' + +void call(Map parameters = [:]) { + List credentials = [ + [type: 'usernamePassword', id: 'abapCredentialsId', env: ['PIPER_username', 'PIPER_password']] + ] + piperExecuteBin(parameters, STEP_NAME, METADATA_FILE, credentials) +}