From 0d6fb160344e68b3e9a926a78d2f6c2f5a53f42f Mon Sep 17 00:00:00 2001 From: Googlom <36107508+Googlom@users.noreply.github.com> Date: Thu, 23 Nov 2023 13:42:02 +0500 Subject: [PATCH 1/8] add deprecation warning (#4691) Co-authored-by: Gulom Alimov --- cmd/checkIfStepActive.go | 3 +++ cmd/getDefaults.go | 3 +++ 2 files changed, 6 insertions(+) diff --git a/cmd/checkIfStepActive.go b/cmd/checkIfStepActive.go index 473984fa14..ae39623f89 100644 --- a/cmd/checkIfStepActive.go +++ b/cmd/checkIfStepActive.go @@ -92,6 +92,9 @@ func checkIfStepActive(utils piperutils.FileUtils) error { runSteps = runConfigV1.RunSteps runStages = runConfigV1.RunStages } else { + log.Entry().Warning("This step is using deprecated format of stage conditions which will be removed in Jan 2024. " + + "To avoid pipeline breakage, please call checkIfStepActive command with --useV1 flag.", + ) runConfig := &config.RunConfig{StageConfigFile: stageConfigFile} err = runConfig.InitRunConfig(projectConfig, nil, nil, nil, nil, doublestar.Glob, checkStepActiveOptions.openFile) if err != nil { diff --git a/cmd/getDefaults.go b/cmd/getDefaults.go index 98b2bd998a..c69ddd056e 100644 --- a/cmd/getDefaults.go +++ b/cmd/getDefaults.go @@ -81,6 +81,9 @@ func getDefaults() ([]map[string]string, error) { var yamlContent string if !defaultsOptions.useV1 { + log.Entry().Warning("This step is using deprecated format of stage conditions which will be removed in Jan 2024. " + + "To avoid pipeline breakage, please call getDefaults command with --useV1 flag.", + ) var c config.Config c.ReadConfig(fc) From 0006f10918f8365cc4cabcbe6dfee06c1b6eaa67 Mon Sep 17 00:00:00 2001 From: Googlom <36107508+Googlom@users.noreply.github.com> Date: Thu, 23 Nov 2023 16:21:40 +0500 Subject: [PATCH 2/8] fix log downloading in GH orchestrator (#4683) Co-authored-by: Gulom Alimov --- pkg/orchestrator/gitHubActions.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pkg/orchestrator/gitHubActions.go b/pkg/orchestrator/gitHubActions.go index 9847fbacce..ba737c456f 100644 --- a/pkg/orchestrator/gitHubActions.go +++ b/pkg/orchestrator/gitHubActions.go @@ -100,7 +100,13 @@ func (g *GitHubActionsConfigProvider) GetLog() ([]byte, error) { wg.Go(func() error { _, resp, err := g.client.Actions.GetWorkflowJobLogs(g.ctx, g.owner, g.repo, jobs[i].ID, true) if err != nil { - return errors.Wrap(err, "fetching job logs failed") + // GetWorkflowJobLogs returns "200 OK" as error when log download is successful. + // Therefore, ignore this error. + // GitHub API returns redirect URL instead of plain text logs. See: + // https://docs.github.com/en/enterprise-server@3.9/rest/actions/workflow-jobs?apiVersion=2022-11-28#download-job-logs-for-a-workflow-run + if err.Error() != "unexpected status code: 200 OK" { + return errors.Wrap(err, "fetching job logs failed") + } } defer resp.Body.Close() From 0baa6a6fcb643d44656f96167de876a3866fc204 Mon Sep 17 00:00:00 2001 From: Pavel Busko Date: Thu, 23 Nov 2023 13:37:19 +0100 Subject: [PATCH 3/8] feat(cnbBuild): Use Paketo Jammy builder as default (#4694) --- cmd/cnbBuild_generated.go | 2 +- cmd/cnbBuild_test.go | 4 ++-- resources/metadata/cnbBuild.yaml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/cnbBuild_generated.go b/cmd/cnbBuild_generated.go index 6e336c6cdd..5ff450c700 100644 --- a/cmd/cnbBuild_generated.go +++ b/cmd/cnbBuild_generated.go @@ -519,7 +519,7 @@ func cnbBuildMetadata() config.StepData { }, }, Containers: []config.Container{ - {Image: "paketobuildpacks/builder:base", Options: []config.Option{{Name: "-u", Value: "0"}}}, + {Image: "paketobuildpacks/builder-jammy-base:latest", Options: []config.Option{{Name: "-u", Value: "0"}}}, }, Outputs: config.StepOutputs{ Resources: []config.StepResources{ diff --git a/cmd/cnbBuild_test.go b/cmd/cnbBuild_test.go index cbfbae2661..cf62cc5bc7 100644 --- a/cmd/cnbBuild_test.go +++ b/cmd/cnbBuild_test.go @@ -144,7 +144,7 @@ func TestRunCnbBuild(t *testing.T) { assert.Contains(t, runner.Calls[1].Params, "my-process") assert.Equal(t, config.ContainerRegistryURL, commonPipelineEnvironment.container.registryURL) assert.Equal(t, "my-image:0.0.1", commonPipelineEnvironment.container.imageNameTag) - assert.Equal(t, `{"cnbBuild":[{"dockerImage":"paketobuildpacks/builder:base"}]}`, commonPipelineEnvironment.custom.buildSettingsInfo) + assert.Equal(t, `{"cnbBuild":[{"dockerImage":"paketobuildpacks/builder-jammy-base:latest"}]}`, commonPipelineEnvironment.custom.buildSettingsInfo) }) t.Run("prefers project descriptor", func(t *testing.T) { @@ -620,7 +620,7 @@ uri = "some-buildpack"`)) assert.Equal(t, "folder", string(customData.Data[0].Path)) assert.Contains(t, customData.Data[0].AdditionalTags, "latest") assert.Contains(t, customData.Data[0].BindingKeys, "SECRET") - assert.Equal(t, "paketobuildpacks/builder:base", customData.Data[0].Builder) + assert.Equal(t, "paketobuildpacks/builder-jammy-base:latest", customData.Data[0].Builder) assert.Contains(t, customData.Data[0].Buildpacks.FromConfig, "paketobuildpacks/java") assert.NotContains(t, customData.Data[0].Buildpacks.FromProjectDescriptor, "paketobuildpacks/java") diff --git a/resources/metadata/cnbBuild.yaml b/resources/metadata/cnbBuild.yaml index b28b7e7052..947ebf5278 100644 --- a/resources/metadata/cnbBuild.yaml +++ b/resources/metadata/cnbBuild.yaml @@ -362,7 +362,7 @@ spec: - filePattern: "**/bom-*.xml" type: sbom containers: - - image: "paketobuildpacks/builder:base" + - image: "paketobuildpacks/builder-jammy-base:latest" options: - name: -u value: "0" From c6c02fc31d44bc1d5711ba2ebba84c48067a8bf0 Mon Sep 17 00:00:00 2001 From: Vyacheslav Starostin <32613074+vstarostin@users.noreply.github.com> Date: Mon, 27 Nov 2023 17:04:49 +0600 Subject: [PATCH 4/8] orchestrator(GHActions): align GetJobURL method with Piper's expectations (#4685) * Align build and job urls with what is expected by piper * Add comments, delete unused func * Clean up * Update tests * Update GetJobURL * Fix test * Update * Clean up --- pkg/orchestrator/gitHubActions.go | 57 ++++++++---------- pkg/orchestrator/gitHubActions_test.go | 80 +++++++++++--------------- pkg/telemetry/telemetry.go | 11 ++-- 3 files changed, 62 insertions(+), 86 deletions(-) diff --git a/pkg/orchestrator/gitHubActions.go b/pkg/orchestrator/gitHubActions.go index ba737c456f..ee68d92443 100644 --- a/pkg/orchestrator/gitHubActions.go +++ b/pkg/orchestrator/gitHubActions.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "io" + "regexp" "strconv" "strings" "sync" @@ -26,7 +27,6 @@ type GitHubActionsConfigProvider struct { runData run jobs []job jobsFetched bool - currentJob job } type run struct { @@ -180,18 +180,21 @@ func (g *GitHubActionsConfigProvider) GetReference() string { return getEnv("GITHUB_REF", "n/a") } -// GetBuildURL returns the builds URL. For example, https://github.com/SAP/jenkins-library/actions/runs/5815297487 +// GetBuildURL returns the builds URL. The URL should point to the pipeline (not to the stage) +// that is currently being executed. For example, https://github.com/SAP/jenkins-library/actions/runs/5815297487 func (g *GitHubActionsConfigProvider) GetBuildURL() string { return g.GetRepoURL() + "/actions/runs/" + g.GetBuildID() } -// GetJobURL returns the current job HTML URL (not API URL). -// For example, https://github.com/SAP/jenkins-library/actions/runs/123456/jobs/7654321 +// GetJobURL returns the job URL. The URL should point to project’s pipelines. +// For example, https://github.com/SAP/jenkins-library/actions/workflows/workflow-file-name.yaml func (g *GitHubActionsConfigProvider) GetJobURL() string { - // We need to query the GitHub API here because the environment variable GITHUB_JOB returns - // the name of the job, not a numeric ID (which we need to form the URL) - g.guessCurrentJob() - return g.currentJob.HtmlURL + fileName := workflowFileName() + if fileName == "" { + return "" + } + + return g.GetRepoURL() + "/actions/workflows/" + fileName } // GetJobName returns the current workflow name. For example, "Piper workflow" @@ -301,32 +304,6 @@ func convertJobs(jobs []*github.WorkflowJob) []job { return result } -func (g *GitHubActionsConfigProvider) guessCurrentJob() { - // check if the current job has already been guessed - if g.currentJob.ID != 0 { - return - } - - // fetch jobs if they haven't been fetched yet - if err := g.fetchJobs(); err != nil { - log.Entry().Errorf("failed to fetch jobs: %s", err) - g.jobs = []job{} - return - } - - targetJobName := getEnv("GITHUB_JOB", "unknown") - log.Entry().Debugf("looking for job '%s' in jobs list: %v", targetJobName, g.jobs) - for _, j := range g.jobs { - // j.Name may be something like "piper / Init / Init" - // but GITHUB_JOB env may contain only "Init" - if strings.HasSuffix(j.Name, targetJobName) { - log.Entry().Debugf("current job id: %d", j.ID) - g.currentJob = j - return - } - } -} - func (g *GitHubActionsConfigProvider) runIdInt64() (int64, error) { strRunId := g.GetBuildID() runId, err := strconv.ParseInt(strRunId, 10, 64) @@ -347,3 +324,15 @@ func getOwnerAndRepoNames() (string, string) { return s[0], s[1] } + +func workflowFileName() string { + workflowRef := getEnv("GITHUB_WORKFLOW_REF", "") + re := regexp.MustCompile(`\.github/workflows/([a-zA-Z0-9_-]+\.(yml|yaml))`) + matches := re.FindStringSubmatch(workflowRef) + if len(matches) > 1 { + return matches[1] + } + + log.Entry().Debugf("unable to determine workflow file name from GITHUB_WORKFLOW_REF: %s", workflowRef) + return "" +} diff --git a/pkg/orchestrator/gitHubActions_test.go b/pkg/orchestrator/gitHubActions_test.go index c8aa8e632e..6a18c5c9a8 100644 --- a/pkg/orchestrator/gitHubActions_test.go +++ b/pkg/orchestrator/gitHubActions_test.go @@ -104,50 +104,6 @@ func TestGitHubActionsConfigProvider_GetPullRequestConfig(t *testing.T) { } } -func TestGitHubActionsConfigProvider_guessCurrentJob(t *testing.T) { - tests := []struct { - name string - jobs []job - jobsFetched bool - targetJobName string - wantJob job - }{ - { - name: "job found", - jobs: []job{{Name: "Job1"}, {Name: "Job2"}, {Name: "Job3"}}, - jobsFetched: true, - targetJobName: "Job2", - wantJob: job{Name: "Job2"}, - }, - { - name: "job found", - jobs: []job{{Name: "Piper / Job1"}, {Name: "Piper / Job2"}, {Name: "Piper / Job3"}}, - jobsFetched: true, - targetJobName: "Job2", - wantJob: job{Name: "Piper / Job2"}, - }, - { - name: "job not found", - jobs: []job{{Name: "Job1"}, {Name: "Job2"}, {Name: "Job3"}}, - jobsFetched: true, - targetJobName: "Job123", - wantJob: job{}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - g := &GitHubActionsConfigProvider{ - jobs: tt.jobs, - jobsFetched: tt.jobsFetched, - } - _ = os.Setenv("GITHUB_JOB", tt.targetJobName) - g.guessCurrentJob() - - assert.Equal(t, tt.wantJob, g.currentJob) - }) - } -} - func TestGitHubActionsConfigProvider_fetchRunData(t *testing.T) { // data respJson := map[string]interface{}{ @@ -325,6 +281,7 @@ func TestGitHubActionsConfigProvider_Others(t *testing.T) { _ = os.Setenv("GITHUB_API_URL", "https://api.github.com") _ = os.Setenv("GITHUB_SERVER_URL", "https://github.com") _ = os.Setenv("GITHUB_REPOSITORY", "SAP/jenkins-library") + _ = os.Setenv("GITHUB_WORKFLOW_REF", "SAP/jenkins-library/.github/workflows/piper.yml@refs/heads/main") p := GitHubActionsConfigProvider{} startedAt, _ := time.Parse(time.RFC3339, "2023-08-11T07:28:24Z") @@ -333,7 +290,6 @@ func TestGitHubActionsConfigProvider_Others(t *testing.T) { Status: "", StartedAt: startedAt, } - p.currentJob = job{ID: 111, Name: "job1", HtmlURL: "https://github.com/SAP/jenkins-library/actions/runs/123456/jobs/7654321"} assert.Equal(t, "n/a", p.OrchestratorVersion()) assert.Equal(t, "GitHubActions", p.OrchestratorType()) @@ -344,10 +300,42 @@ func TestGitHubActionsConfigProvider_Others(t *testing.T) { assert.Equal(t, "main", p.GetBranch()) assert.Equal(t, "refs/pull/42/merge", p.GetReference()) assert.Equal(t, "https://github.com/SAP/jenkins-library/actions/runs/11111", p.GetBuildURL()) - assert.Equal(t, "https://github.com/SAP/jenkins-library/actions/runs/123456/jobs/7654321", p.GetJobURL()) + assert.Equal(t, "https://github.com/SAP/jenkins-library/actions/workflows/piper.yml", p.GetJobURL()) assert.Equal(t, "Piper workflow", p.GetJobName()) assert.Equal(t, "ffac537e6cbbf934b08745a378932722df287a53", p.GetCommit()) assert.Equal(t, "https://api.github.com/repos/SAP/jenkins-library/actions", actionsURL()) assert.True(t, p.IsPullRequest()) assert.True(t, isGitHubActions()) } + +func TestWorkflowFileName(t *testing.T) { + defer resetEnv(os.Environ()) + os.Clearenv() + + tests := []struct { + name, workflowRef, want string + }{ + { + name: "valid file name (yaml)", + workflowRef: "owner/repo/.github/workflows/test-workflow.yaml@refs/heads/main", + want: "test-workflow.yaml", + }, + { + name: "valid file name (yml)", + workflowRef: "owner/repo/.github/workflows/test-workflow.yml@refs/heads/main", + want: "test-workflow.yml", + }, + { + name: "invalid file name", + workflowRef: "owner/repo/.github/workflows/test-workflow@refs/heads/main", + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _ = os.Setenv("GITHUB_WORKFLOW_REF", tt.workflowRef) + result := workflowFileName() + assert.Equal(t, tt.want, result) + }) + } +} diff --git a/pkg/telemetry/telemetry.go b/pkg/telemetry/telemetry.go index c3f0c34f10..e262010be0 100644 --- a/pkg/telemetry/telemetry.go +++ b/pkg/telemetry/telemetry.go @@ -4,15 +4,14 @@ import ( "crypto/sha1" "encoding/json" "fmt" - "github.com/SAP/jenkins-library/pkg/orchestrator" - "strconv" - "time" - "net/http" "net/url" + "strconv" + "time" piperhttp "github.com/SAP/jenkins-library/pkg/http" "github.com/SAP/jenkins-library/pkg/log" + "github.com/SAP/jenkins-library/pkg/orchestrator" ) // eventType @@ -81,8 +80,8 @@ func (t *Telemetry) Initialize(telemetryDisabled bool, stepName string) { EventType: eventType, StepName: stepName, SiteID: t.SiteID, - PipelineURLHash: t.getPipelineURLHash(), // http://server:port/jenkins/job/foo/ - BuildURLHash: t.getBuildURLHash(), // http://server:port/jenkins/job/foo/15/ + PipelineURLHash: t.getPipelineURLHash(), // URL (hashed value) which points to the project’s pipelines + BuildURLHash: t.getBuildURLHash(), // URL (hashed value) which points to the pipeline that is currently running } t.baseMetaData = baseMetaData } From 17de9ed34ca576b75923cbfc75d32d60cae8f8ac Mon Sep 17 00:00:00 2001 From: Oliver Feldmann Date: Mon, 27 Nov 2023 14:28:18 +0100 Subject: [PATCH 5/8] Allow cALM service key for cTMS steps (#4661) * Allow cALM service keys * Fix typo Co-authored-by: Srinikitha Kondreddy * fix typo Co-authored-by: Srinikitha Kondreddy * Hardcode tms endpoint in calm test case * Add new serviceKey parameter * Use new serviceKey parameter With deprecation warning if old tmsServiceKey parameter is used * Add unit tests and optimise * Remove tms from service key log message * Apply suggestions from code review Co-authored-by: Artem Bannikov <62880541+artembannikov@users.noreply.github.com> * Remove unused json fields mapping * Apply review suggestion * Apply further review suggestions * Use new parameter name in groovy * Generate again * Fix groovy test --------- Co-authored-by: Srinikitha Kondreddy Co-authored-by: Artem Bannikov <62880541+artembannikov@users.noreply.github.com> --- cmd/tmsExport.go | 6 +++- cmd/tmsExport_generated.go | 26 ++++++++++++----- cmd/tmsExport_test.go | 47 +++++++++++++++++++++++++++++++ cmd/tmsUpload.go | 6 +++- cmd/tmsUpload_generated.go | 26 ++++++++++++----- cmd/tmsUpload_test.go | 47 +++++++++++++++++++++++++++++++ pkg/tms/tmsUtils.go | 40 +++++++++++++++++--------- pkg/tms/tmsUtils_test.go | 47 +++++++++++++++++++++++++++++++ resources/metadata/tmsExport.yaml | 26 ++++++++++++++--- resources/metadata/tmsUpload.yaml | 26 ++++++++++++++--- test/groovy/TmsUploadTest.groovy | 2 +- vars/tmsExport.groovy | 2 +- vars/tmsUpload.groovy | 2 +- 13 files changed, 263 insertions(+), 40 deletions(-) create mode 100644 pkg/tms/tmsUtils_test.go diff --git a/cmd/tmsExport.go b/cmd/tmsExport.go index 5b448edd7d..ec9c4ed822 100644 --- a/cmd/tmsExport.go +++ b/cmd/tmsExport.go @@ -49,7 +49,11 @@ func runTmsExport(exportConfig tmsExportOptions, communicationInstance tms.Commu func convertExportOptions(exportConfig tmsExportOptions) tms.Options { var config tms.Options - config.TmsServiceKey = exportConfig.TmsServiceKey + config.ServiceKey = exportConfig.ServiceKey + if exportConfig.ServiceKey == "" && exportConfig.TmsServiceKey != "" { + config.ServiceKey = exportConfig.TmsServiceKey + log.Entry().Warn("DEPRECATION WARNING: The tmsServiceKey parameter has been deprecated, please use the serviceKey parameter instead.") + } config.CustomDescription = exportConfig.CustomDescription if config.CustomDescription == "" { config.CustomDescription = tms.DEFAULT_TR_DESCRIPTION diff --git a/cmd/tmsExport_generated.go b/cmd/tmsExport_generated.go index d7a69ae17a..1379ce1271 100644 --- a/cmd/tmsExport_generated.go +++ b/cmd/tmsExport_generated.go @@ -19,6 +19,7 @@ import ( type tmsExportOptions struct { TmsServiceKey string `json:"tmsServiceKey,omitempty"` + ServiceKey string `json:"serviceKey,omitempty"` CustomDescription string `json:"customDescription,omitempty"` NamedUser string `json:"namedUser,omitempty"` NodeName string `json:"nodeName,omitempty"` @@ -83,7 +84,7 @@ For more information, see [official documentation of SAP Cloud Transport Managem !!! note "Prerequisites" * You have subscribed to and set up TMS, as described in [Initial Setup](https://help.sap.com/viewer/7f7160ec0d8546c6b3eab72fb5ad6fd8/Cloud/en-US/66fd7283c62f48adb23c56fb48c84a60.html), which includes the configuration of your transport landscape. -* A corresponding service key has been created, as described in [Set Up the Environment to Transport Content Archives directly in an Application](https://help.sap.com/viewer/7f7160ec0d8546c6b3eab72fb5ad6fd8/Cloud/en-US/8d9490792ed14f1bbf8a6ac08a6bca64.html). This service key (JSON) must be stored as a secret text within the Jenkins secure store or provided as value of tmsServiceKey parameter.`, +* A corresponding service key has been created, as described in [Set Up the Environment to Transport Content Archives directly in an Application](https://help.sap.com/viewer/7f7160ec0d8546c6b3eab72fb5ad6fd8/Cloud/en-US/8d9490792ed14f1bbf8a6ac08a6bca64.html). This service key (JSON) must be stored as a secret text within the Jenkins secure store or provided as value of serviceKey parameter.`, PreRunE: func(cmd *cobra.Command, _ []string) error { startTime = time.Now() log.SetStepName(STEP_NAME) @@ -101,6 +102,7 @@ For more information, see [official documentation of SAP Cloud Transport Managem return err } log.RegisterSecret(stepConfig.TmsServiceKey) + log.RegisterSecret(stepConfig.ServiceKey) if len(GeneralConfig.HookConfig.SentryConfig.Dsn) > 0 { sentryHook := log.NewSentryHook(GeneralConfig.HookConfig.SentryConfig.Dsn, GeneralConfig.CorrelationID) @@ -170,7 +172,8 @@ For more information, see [official documentation of SAP Cloud Transport Managem } func addTmsExportFlags(cmd *cobra.Command, stepConfig *tmsExportOptions) { - cmd.Flags().StringVar(&stepConfig.TmsServiceKey, "tmsServiceKey", os.Getenv("PIPER_tmsServiceKey"), "Service key JSON string to access the SAP Cloud Transport Management service instance APIs. If not specified and if pipeline is running on Jenkins, service key, stored under ID provided with credentialsId parameter, is used.") + cmd.Flags().StringVar(&stepConfig.TmsServiceKey, "tmsServiceKey", os.Getenv("PIPER_tmsServiceKey"), "DEPRECATION WARNING: This parameter has been deprecated, please use the serviceKey parameter instead, which supports both service key for TMS (SAP Cloud Transport Management service), as well as service key for CALM (SAP Cloud Application Lifecycle Management) service.\nService key JSON string to access the SAP Cloud Transport Management service instance APIs.\n") + cmd.Flags().StringVar(&stepConfig.ServiceKey, "serviceKey", os.Getenv("PIPER_serviceKey"), "Service key JSON string to access TMS (SAP Cloud Transport Management service) instance APIs. This can be a service key for TMS, or a service key for CALM (SAP Cloud Application Lifecycle Management) service. If not specified and if pipeline is running on Jenkins, service key, stored under ID provided with credentialsId parameter, is used.\n") cmd.Flags().StringVar(&stepConfig.CustomDescription, "customDescription", os.Getenv("PIPER_customDescription"), "Can be used as the description of a transport request. Will overwrite the default, which is corresponding Git commit ID.") cmd.Flags().StringVar(&stepConfig.NamedUser, "namedUser", `Piper-Pipeline`, "Defines the named user to execute transport request with. The default value is 'Piper-Pipeline'. If pipeline is running on Jenkins, the name of the user, who started the job, is tried to be used at first.") cmd.Flags().StringVar(&stepConfig.NodeName, "nodeName", os.Getenv("PIPER_nodeName"), "Defines the name of the export node - starting node in TMS landscape. The transport request is added to the queues of the follow-on nodes of export node.") @@ -179,7 +182,7 @@ func addTmsExportFlags(cmd *cobra.Command, stepConfig *tmsExportOptions) { cmd.Flags().StringVar(&stepConfig.Proxy, "proxy", os.Getenv("PIPER_proxy"), "Proxy URL which should be used for communication with the SAP Cloud Transport Management service backend.") - cmd.MarkFlagRequired("tmsServiceKey") + cmd.MarkFlagRequired("serviceKey") cmd.MarkFlagRequired("nodeName") } @@ -194,18 +197,27 @@ func tmsExportMetadata() config.StepData { Spec: config.StepSpec{ Inputs: config.StepInputs{ Secrets: []config.StepSecrets{ - {Name: "credentialsId", Description: "Jenkins 'Secret text' credentials ID containing service key for SAP Cloud Transport Management service.", Type: "jenkins"}, + {Name: "credentialsId", Description: "Jenkins 'Secret text' credentials ID containing service key for TMS (SAP Cloud Transport Management service) or CALM (SAP Cloud Application Lifecycle Management) service.", Type: "jenkins"}, }, Resources: []config.StepResources{ {Name: "buildResult", Type: "stash"}, }, Parameters: []config.StepParameters{ { - Name: "tmsServiceKey", + Name: "tmsServiceKey", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STEPS", "STAGES"}, + Type: "string", + Mandatory: false, + Aliases: []config.Alias{}, + Default: os.Getenv("PIPER_tmsServiceKey"), + }, + { + Name: "serviceKey", ResourceRef: []config.ResourceReference{ { Name: "credentialsId", - Param: "tmsServiceKey", + Param: "serviceKey", Type: "secret", }, }, @@ -213,7 +225,7 @@ func tmsExportMetadata() config.StepData { Type: "string", Mandatory: true, Aliases: []config.Alias{}, - Default: os.Getenv("PIPER_tmsServiceKey"), + Default: os.Getenv("PIPER_serviceKey"), }, { Name: "customDescription", diff --git a/cmd/tmsExport_test.go b/cmd/tmsExport_test.go index 39e34e8c38..72e43967b7 100644 --- a/cmd/tmsExport_test.go +++ b/cmd/tmsExport_test.go @@ -150,3 +150,50 @@ func TestRunTmsExport(t *testing.T) { assert.EqualError(t, err, "failed to export file to node: Something went wrong on exporting file to node") }) } + +func Test_convertExportOptions(t *testing.T) { + t.Parallel() + mockServiceKey := `no real serviceKey json necessary for these tests` + + t.Run("Use of new serviceKey parameter works", func(t *testing.T) { + t.Parallel() + + // init + config := tmsExportOptions{ServiceKey: mockServiceKey} + wantOptions := tms.Options{ServiceKey: mockServiceKey, CustomDescription: "Created by Piper"} + + // test + gotOptions := convertExportOptions(config) + + // assert + assert.Equal(t, wantOptions, gotOptions) + }) + + t.Run("Use of old tmsServiceKey parameter works as well", func(t *testing.T) { + t.Parallel() + + // init + config := tmsExportOptions{TmsServiceKey: mockServiceKey} + wantOptions := tms.Options{ServiceKey: mockServiceKey, CustomDescription: "Created by Piper"} + + // test + gotOptions := convertExportOptions(config) + + // assert + assert.Equal(t, wantOptions, gotOptions) + }) + + t.Run("Use of both tmsServiceKey and serviceKey parameter favors the new serviceKey parameter", func(t *testing.T) { + t.Parallel() + + // init + config := tmsExportOptions{ServiceKey: mockServiceKey, TmsServiceKey: "some other string"} + wantOptions := tms.Options{ServiceKey: mockServiceKey, CustomDescription: "Created by Piper"} + + // test + gotOptions := convertExportOptions(config) + + // assert + assert.Equal(t, wantOptions, gotOptions) + }) +} diff --git a/cmd/tmsUpload.go b/cmd/tmsUpload.go index 0462326646..9fff8f938e 100644 --- a/cmd/tmsUpload.go +++ b/cmd/tmsUpload.go @@ -42,7 +42,11 @@ func runTmsUpload(uploadConfig tmsUploadOptions, communicationInstance tms.Commu func convertUploadOptions(uploadConfig tmsUploadOptions) tms.Options { var config tms.Options - config.TmsServiceKey = uploadConfig.TmsServiceKey + config.ServiceKey = uploadConfig.ServiceKey + if uploadConfig.ServiceKey == "" && uploadConfig.TmsServiceKey != "" { + config.ServiceKey = uploadConfig.TmsServiceKey + log.Entry().Warn("DEPRECATION WARNING: The tmsServiceKey parameter has been deprecated, please use the serviceKey parameter instead.") + } config.CustomDescription = uploadConfig.CustomDescription if config.CustomDescription == "" { config.CustomDescription = tms.DEFAULT_TR_DESCRIPTION diff --git a/cmd/tmsUpload_generated.go b/cmd/tmsUpload_generated.go index 5e2b08aa76..547e3bfcf5 100644 --- a/cmd/tmsUpload_generated.go +++ b/cmd/tmsUpload_generated.go @@ -19,6 +19,7 @@ import ( type tmsUploadOptions struct { TmsServiceKey string `json:"tmsServiceKey,omitempty"` + ServiceKey string `json:"serviceKey,omitempty"` CustomDescription string `json:"customDescription,omitempty"` NamedUser string `json:"namedUser,omitempty"` NodeName string `json:"nodeName,omitempty"` @@ -84,7 +85,7 @@ For more information, see [official documentation of SAP Cloud Transport Managem !!! note "Prerequisites" * You have subscribed to and set up TMS, as described in [Initial Setup](https://help.sap.com/viewer/7f7160ec0d8546c6b3eab72fb5ad6fd8/Cloud/en-US/66fd7283c62f48adb23c56fb48c84a60.html), which includes the configuration of a node to be used for uploading an MTA file. -* A corresponding service key has been created, as described in [Set Up the Environment to Transport Content Archives directly in an Application](https://help.sap.com/viewer/7f7160ec0d8546c6b3eab72fb5ad6fd8/Cloud/en-US/8d9490792ed14f1bbf8a6ac08a6bca64.html). This service key (JSON) must be stored as a secret text within the Jenkins secure store or provided as value of tmsServiceKey parameter.`, +* A corresponding service key has been created, as described in [Set Up the Environment to Transport Content Archives directly in an Application](https://help.sap.com/viewer/7f7160ec0d8546c6b3eab72fb5ad6fd8/Cloud/en-US/8d9490792ed14f1bbf8a6ac08a6bca64.html). This service key (JSON) must be stored as a secret text within the Jenkins secure store or provided as value of serviceKey parameter.`, PreRunE: func(cmd *cobra.Command, _ []string) error { startTime = time.Now() log.SetStepName(STEP_NAME) @@ -102,6 +103,7 @@ For more information, see [official documentation of SAP Cloud Transport Managem return err } log.RegisterSecret(stepConfig.TmsServiceKey) + log.RegisterSecret(stepConfig.ServiceKey) if len(GeneralConfig.HookConfig.SentryConfig.Dsn) > 0 { sentryHook := log.NewSentryHook(GeneralConfig.HookConfig.SentryConfig.Dsn, GeneralConfig.CorrelationID) @@ -171,7 +173,8 @@ For more information, see [official documentation of SAP Cloud Transport Managem } func addTmsUploadFlags(cmd *cobra.Command, stepConfig *tmsUploadOptions) { - cmd.Flags().StringVar(&stepConfig.TmsServiceKey, "tmsServiceKey", os.Getenv("PIPER_tmsServiceKey"), "Service key JSON string to access the SAP Cloud Transport Management service instance APIs. If not specified and if pipeline is running on Jenkins, service key, stored under ID provided with credentialsId parameter, is used.") + cmd.Flags().StringVar(&stepConfig.TmsServiceKey, "tmsServiceKey", os.Getenv("PIPER_tmsServiceKey"), "DEPRECATION WARNING: This parameter has been deprecated, please use the serviceKey parameter instead, which supports both service key for TMS (SAP Cloud Transport Management service), as well as service key for CALM (SAP Cloud Application Lifecycle Management) service.\nService key JSON string to access the SAP Cloud Transport Management service instance APIs.\n") + cmd.Flags().StringVar(&stepConfig.ServiceKey, "serviceKey", os.Getenv("PIPER_serviceKey"), "Service key JSON string to access TMS (SAP Cloud Transport Management service) instance APIs. This can be a service key for TMS, or a service key for CALM (SAP Cloud Application Lifecycle Management) service. If not specified and if pipeline is running on Jenkins, service key, stored under ID provided with credentialsId parameter, is used.\n") cmd.Flags().StringVar(&stepConfig.CustomDescription, "customDescription", os.Getenv("PIPER_customDescription"), "Can be used as the description of a transport request. Will overwrite the default, which is corresponding Git commit ID.") cmd.Flags().StringVar(&stepConfig.NamedUser, "namedUser", `Piper-Pipeline`, "Defines the named user to execute transport request with. The default value is 'Piper-Pipeline'. If pipeline is running on Jenkins, the name of the user, who started the job, is tried to be used at first.") cmd.Flags().StringVar(&stepConfig.NodeName, "nodeName", os.Getenv("PIPER_nodeName"), "Defines the name of the node to which the *.mtar file should be uploaded.") @@ -181,7 +184,7 @@ func addTmsUploadFlags(cmd *cobra.Command, stepConfig *tmsUploadOptions) { cmd.Flags().StringVar(&stepConfig.Proxy, "proxy", os.Getenv("PIPER_proxy"), "Proxy URL which should be used for communication with the SAP Cloud Transport Management service backend.") cmd.Flags().StringSliceVar(&stepConfig.StashContent, "stashContent", []string{`buildResult`}, "If specific stashes should be considered during Jenkins execution, their names need to be passed as a list via this parameter, e.g. stashContent: [\"deployDescriptor\", \"buildResult\"]. By default, the build result is considered.") - cmd.MarkFlagRequired("tmsServiceKey") + cmd.MarkFlagRequired("serviceKey") cmd.MarkFlagRequired("nodeName") } @@ -196,18 +199,27 @@ func tmsUploadMetadata() config.StepData { Spec: config.StepSpec{ Inputs: config.StepInputs{ Secrets: []config.StepSecrets{ - {Name: "credentialsId", Description: "Jenkins 'Secret text' credentials ID containing service key for SAP Cloud Transport Management service.", Type: "jenkins"}, + {Name: "credentialsId", Description: "Jenkins 'Secret text' credentials ID containing service key for TMS (SAP Cloud Transport Management service) or CALM (SAP Cloud Application Lifecycle Management) service.", Type: "jenkins"}, }, Resources: []config.StepResources{ {Name: "buildResult", Type: "stash"}, }, Parameters: []config.StepParameters{ { - Name: "tmsServiceKey", + Name: "tmsServiceKey", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STEPS", "STAGES"}, + Type: "string", + Mandatory: false, + Aliases: []config.Alias{}, + Default: os.Getenv("PIPER_tmsServiceKey"), + }, + { + Name: "serviceKey", ResourceRef: []config.ResourceReference{ { Name: "credentialsId", - Param: "tmsServiceKey", + Param: "serviceKey", Type: "secret", }, }, @@ -215,7 +227,7 @@ func tmsUploadMetadata() config.StepData { Type: "string", Mandatory: true, Aliases: []config.Alias{}, - Default: os.Getenv("PIPER_tmsServiceKey"), + Default: os.Getenv("PIPER_serviceKey"), }, { Name: "customDescription", diff --git a/cmd/tmsUpload_test.go b/cmd/tmsUpload_test.go index a322c868f5..40a1c0f63a 100644 --- a/cmd/tmsUpload_test.go +++ b/cmd/tmsUpload_test.go @@ -506,3 +506,50 @@ func TestRunTmsUpload(t *testing.T) { assert.EqualError(t, err, "failed to upload file to node: Something went wrong on uploading file to node") }) } + +func Test_convertUploadOptions(t *testing.T) { + t.Parallel() + mockServiceKey := `no real serviceKey json necessary for these tests` + + t.Run("Use of new serviceKey parameter works", func(t *testing.T) { + t.Parallel() + + // init + config := tmsUploadOptions{ServiceKey: mockServiceKey} + wantOptions := tms.Options{ServiceKey: mockServiceKey, CustomDescription: "Created by Piper"} + + // test + gotOptions := convertUploadOptions(config) + + // assert + assert.Equal(t, wantOptions, gotOptions) + }) + + t.Run("Use of old tmsServiceKey parameter works as well", func(t *testing.T) { + t.Parallel() + + // init + config := tmsUploadOptions{TmsServiceKey: mockServiceKey} + wantOptions := tms.Options{ServiceKey: mockServiceKey, CustomDescription: "Created by Piper"} + + // test + gotOptions := convertUploadOptions(config) + + // assert + assert.Equal(t, wantOptions, gotOptions) + }) + + t.Run("Use of both tmsServiceKey and serviceKey parameter favors the new serviceKey parameter", func(t *testing.T) { + t.Parallel() + + // init + config := tmsUploadOptions{ServiceKey: mockServiceKey, TmsServiceKey: "some other string"} + wantOptions := tms.Options{ServiceKey: mockServiceKey, CustomDescription: "Created by Piper"} + + // test + gotOptions := convertUploadOptions(config) + + // assert + assert.Equal(t, wantOptions, gotOptions) + }) +} diff --git a/pkg/tms/tmsUtils.go b/pkg/tms/tmsUtils.go index 91c828dc0b..416d8619bb 100644 --- a/pkg/tms/tmsUtils.go +++ b/pkg/tms/tmsUtils.go @@ -28,8 +28,13 @@ type uaa struct { } type serviceKey struct { - Uaa uaa `json:"uaa"` - Uri string `json:"uri"` + Uaa uaa `json:"uaa"` + Uri string `json:"uri"` + CALMEndpoints cALMEndpoints `json:"endpoints"` +} + +type cALMEndpoints *struct { + API string `json:"Api"` } type CommunicationInstance struct { @@ -105,15 +110,15 @@ type CommunicationInterface interface { } type Options struct { - TmsServiceKey string `json:"tmsServiceKey,omitempty"` - CustomDescription string `json:"customDescription,omitempty"` - NamedUser string `json:"namedUser,omitempty"` - NodeName string `json:"nodeName,omitempty"` - MtaPath string `json:"mtaPath,omitempty"` - MtaVersion string `json:"mtaVersion,omitempty"` - NodeExtDescriptorMapping map[string]interface{} `json:"nodeExtDescriptorMapping,omitempty"` - Proxy string `json:"proxy,omitempty"` - StashContent []string `json:"stashContent,omitempty"` + ServiceKey string + CustomDescription string + NamedUser string + NodeName string + MtaPath string + MtaVersion string + NodeExtDescriptorMapping map[string]interface{} + Proxy string + StashContent []string Verbose bool } @@ -123,6 +128,7 @@ type tmsUtilsBundle struct { } const DEFAULT_TR_DESCRIPTION = "Created by Piper" +const CALM_REROUTING_ENDPOINT_TO_CTMS = "/imp-cdm-transport-management-api/v1" func NewTmsUtils() TmsUtils { utils := tmsUtilsBundle{ @@ -140,6 +146,14 @@ func unmarshalServiceKey(serviceKeyJson string) (serviceKey serviceKey, err erro if err != nil { return } + if len(serviceKey.Uri) == 0 { + if serviceKey.CALMEndpoints != nil && len(serviceKey.CALMEndpoints.API) > 0 { + serviceKey.Uri = serviceKey.CALMEndpoints.API + CALM_REROUTING_ENDPOINT_TO_CTMS + } else { + err = fmt.Errorf("neither uri nor endpoints.Api is set in service key json string") + return + } + } return } @@ -237,9 +251,9 @@ func SetupCommunication(config Options) (communicationInstance CommunicationInte } } - serviceKey, err := unmarshalServiceKey(config.TmsServiceKey) + serviceKey, err := unmarshalServiceKey(config.ServiceKey) if err != nil { - log.Entry().WithError(err).Fatal("Failed to unmarshal TMS service key") + log.Entry().WithError(err).Fatal("Failed to unmarshal service key") } log.RegisterSecret(serviceKey.Uaa.ClientSecret) diff --git a/pkg/tms/tmsUtils_test.go b/pkg/tms/tmsUtils_test.go new file mode 100644 index 0000000000..2b166d1f75 --- /dev/null +++ b/pkg/tms/tmsUtils_test.go @@ -0,0 +1,47 @@ +package tms + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_unmarshalServiceKey(t *testing.T) { + tests := []struct { + name string + serviceKeyJson string + wantTmsUrl string + errMessage string + }{ + { + name: "standard cTMS service key uri works", + serviceKeyJson: `{"uri": "https://my.tms.endpoint.sap.com"}`, + wantTmsUrl: "https://my.tms.endpoint.sap.com", + }, + { + name: "standard cALM service key uri has expected postfix", + serviceKeyJson: `{"endpoints": {"Api": "https://my.alm.endpoint.sap.com"}}`, + wantTmsUrl: "https://my.alm.endpoint.sap.com/imp-cdm-transport-management-api/v1", + }, + { + name: "no uri or endpoints in service key leads to error", + serviceKeyJson: `{"missing key options": "leads to error"}`, + errMessage: "neither uri nor endpoints.Api is set in service key json string", + }, + { + name: "faulty json leads to error", + serviceKeyJson: `"this is not correct json"`, + errMessage: "json: cannot unmarshal string into Go value of type tms.serviceKey", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotServiceKey, err := unmarshalServiceKey(tt.serviceKeyJson) + if tt.errMessage == "" { + assert.NoError(t, err, "No error was expected") + assert.Equal(t, tt.wantTmsUrl, gotServiceKey.Uri, "Expected tms url does not match the uri in the service key") + } else { + assert.EqualError(t, err, tt.errMessage, "Error message not as expected") + } + }) + } +} diff --git a/resources/metadata/tmsExport.yaml b/resources/metadata/tmsExport.yaml index 4472f37851..83d561bbae 100644 --- a/resources/metadata/tmsExport.yaml +++ b/resources/metadata/tmsExport.yaml @@ -9,12 +9,12 @@ metadata: !!! note "Prerequisites" * You have subscribed to and set up TMS, as described in [Initial Setup](https://help.sap.com/viewer/7f7160ec0d8546c6b3eab72fb5ad6fd8/Cloud/en-US/66fd7283c62f48adb23c56fb48c84a60.html), which includes the configuration of your transport landscape. - * A corresponding service key has been created, as described in [Set Up the Environment to Transport Content Archives directly in an Application](https://help.sap.com/viewer/7f7160ec0d8546c6b3eab72fb5ad6fd8/Cloud/en-US/8d9490792ed14f1bbf8a6ac08a6bca64.html). This service key (JSON) must be stored as a secret text within the Jenkins secure store or provided as value of tmsServiceKey parameter. + * A corresponding service key has been created, as described in [Set Up the Environment to Transport Content Archives directly in an Application](https://help.sap.com/viewer/7f7160ec0d8546c6b3eab72fb5ad6fd8/Cloud/en-US/8d9490792ed14f1bbf8a6ac08a6bca64.html). This service key (JSON) must be stored as a secret text within the Jenkins secure store or provided as value of serviceKey parameter. spec: inputs: secrets: - name: credentialsId - description: Jenkins 'Secret text' credentials ID containing service key for SAP Cloud Transport Management service. + description: Jenkins 'Secret text' credentials ID containing service key for TMS (SAP Cloud Transport Management service) or CALM (SAP Cloud Application Lifecycle Management) service. type: jenkins resources: - name: buildResult @@ -22,7 +22,25 @@ spec: params: - name: tmsServiceKey type: string - description: Service key JSON string to access the SAP Cloud Transport Management service instance APIs. If not specified and if pipeline is running on Jenkins, service key, stored under ID provided with credentialsId parameter, is used. + description: > + DEPRECATION WARNING: This parameter has been deprecated, please use the serviceKey parameter instead, + which supports both service key for TMS (SAP Cloud Transport Management service), + as well as service key for CALM (SAP Cloud Application Lifecycle Management) service. + + Service key JSON string to access the SAP Cloud Transport Management service instance APIs. + scope: + - PARAMETERS + - STEPS + - STAGES + mandatory: false + secret: true + - name: serviceKey + type: string + description: > + Service key JSON string to access TMS (SAP Cloud Transport Management service) instance APIs. + This can be a service key for TMS, + or a service key for CALM (SAP Cloud Application Lifecycle Management) service. + If not specified and if pipeline is running on Jenkins, service key, stored under ID provided with credentialsId parameter, is used. scope: - PARAMETERS - STEPS @@ -32,7 +50,7 @@ spec: resourceRef: - name: credentialsId type: secret - param: tmsServiceKey + param: serviceKey - name: customDescription type: string description: Can be used as the description of a transport request. Will overwrite the default, which is corresponding Git commit ID. diff --git a/resources/metadata/tmsUpload.yaml b/resources/metadata/tmsUpload.yaml index afe53f9717..7d61e4c31b 100644 --- a/resources/metadata/tmsUpload.yaml +++ b/resources/metadata/tmsUpload.yaml @@ -9,12 +9,12 @@ metadata: !!! note "Prerequisites" * You have subscribed to and set up TMS, as described in [Initial Setup](https://help.sap.com/viewer/7f7160ec0d8546c6b3eab72fb5ad6fd8/Cloud/en-US/66fd7283c62f48adb23c56fb48c84a60.html), which includes the configuration of a node to be used for uploading an MTA file. - * A corresponding service key has been created, as described in [Set Up the Environment to Transport Content Archives directly in an Application](https://help.sap.com/viewer/7f7160ec0d8546c6b3eab72fb5ad6fd8/Cloud/en-US/8d9490792ed14f1bbf8a6ac08a6bca64.html). This service key (JSON) must be stored as a secret text within the Jenkins secure store or provided as value of tmsServiceKey parameter. + * A corresponding service key has been created, as described in [Set Up the Environment to Transport Content Archives directly in an Application](https://help.sap.com/viewer/7f7160ec0d8546c6b3eab72fb5ad6fd8/Cloud/en-US/8d9490792ed14f1bbf8a6ac08a6bca64.html). This service key (JSON) must be stored as a secret text within the Jenkins secure store or provided as value of serviceKey parameter. spec: inputs: secrets: - name: credentialsId - description: Jenkins 'Secret text' credentials ID containing service key for SAP Cloud Transport Management service. + description: Jenkins 'Secret text' credentials ID containing service key for TMS (SAP Cloud Transport Management service) or CALM (SAP Cloud Application Lifecycle Management) service. type: jenkins resources: - name: buildResult @@ -22,7 +22,25 @@ spec: params: - name: tmsServiceKey type: string - description: Service key JSON string to access the SAP Cloud Transport Management service instance APIs. If not specified and if pipeline is running on Jenkins, service key, stored under ID provided with credentialsId parameter, is used. + description: > + DEPRECATION WARNING: This parameter has been deprecated, please use the serviceKey parameter instead, + which supports both service key for TMS (SAP Cloud Transport Management service), + as well as service key for CALM (SAP Cloud Application Lifecycle Management) service. + + Service key JSON string to access the SAP Cloud Transport Management service instance APIs. + scope: + - PARAMETERS + - STEPS + - STAGES + mandatory: false + secret: true + - name: serviceKey + type: string + description: > + Service key JSON string to access TMS (SAP Cloud Transport Management service) instance APIs. + This can be a service key for TMS, + or a service key for CALM (SAP Cloud Application Lifecycle Management) service. + If not specified and if pipeline is running on Jenkins, service key, stored under ID provided with credentialsId parameter, is used. scope: - PARAMETERS - STEPS @@ -32,7 +50,7 @@ spec: resourceRef: - name: credentialsId type: secret - param: tmsServiceKey + param: serviceKey - name: customDescription type: string description: Can be used as the description of a transport request. Will overwrite the default, which is corresponding Git commit ID. diff --git a/test/groovy/TmsUploadTest.groovy b/test/groovy/TmsUploadTest.groovy index ecc2f30dad..54cec2e8c7 100644 --- a/test/groovy/TmsUploadTest.groovy +++ b/test/groovy/TmsUploadTest.groovy @@ -115,7 +115,7 @@ public class TmsUploadTest extends BasePiperTest { // contains assertion does not work apparently when comparing a list of lists against an expected list boolean found = false credInfo.each { entry -> - if (entry == [type: 'token', id: 'credentialsId', env: ['PIPER_tmsServiceKey']]) { + if (entry == [type: 'token', id: 'credentialsId', env: ['PIPER_serviceKey']]) { found = true } } diff --git a/vars/tmsExport.groovy b/vars/tmsExport.groovy index f486429d01..303fa6cc63 100644 --- a/vars/tmsExport.groovy +++ b/vars/tmsExport.groovy @@ -6,7 +6,7 @@ import com.sap.piper.JenkinsUtils void call(Map parameters = [:]) { List credentials = [ - [type: 'token', id: 'credentialsId', env: ['PIPER_tmsServiceKey']] + [type: 'token', id: 'credentialsId', env: ['PIPER_serviceKey']] ] piperExecuteBin(parameters, STEP_NAME, METADATA_FILE, credentials, false, false, true) diff --git a/vars/tmsUpload.groovy b/vars/tmsUpload.groovy index d89203c922..4c586d40af 100644 --- a/vars/tmsUpload.groovy +++ b/vars/tmsUpload.groovy @@ -96,7 +96,7 @@ void call(Map parameters = [:]) { if (config.useGoStep != false) { List credentials = [ - [type: 'token', id: 'credentialsId', env: ['PIPER_tmsServiceKey']] + [type: 'token', id: 'credentialsId', env: ['PIPER_serviceKey']] ] if (namedUser) { From 0a738e882c8d27eee140c19876774638fcdf2482 Mon Sep 17 00:00:00 2001 From: Daniel Mieg <56156797+DanielMieg@users.noreply.github.com> Date: Tue, 28 Nov 2023 13:26:31 +0100 Subject: [PATCH 6/8] [ABAP] Refactor steps to allow API migration (#4687) * Initial API Manager * Intermediate part * Intermediate step * Fix utils tests * Adapt pull * Migrate Checkout * Refactor createTags * Refactoring * Setup tests for SAP_COM_0510 * Add tests * Refactor parsing * Add retry to clone * refactor * Refactor and tests * Fix function call * Adapt create tag tests * Adapt tests * Add tests * Fix tests * Fix test * Fix client mock * Add unit test comments * Add missing parameters * Branch not mandatory for clone * Improve switch branch trigger --------- Co-authored-by: tiloKo <70266685+tiloKo@users.noreply.github.com> --- cmd/abapEnvironmentCheckoutBranch.go | 117 +---- cmd/abapEnvironmentCheckoutBranch_test.go | 107 +--- cmd/abapEnvironmentCloneGitRepo.go | 270 ++++------ cmd/abapEnvironmentCloneGitRepo_test.go | 210 +++----- cmd/abapEnvironmentCreateTag.go | 154 ++---- cmd/abapEnvironmentCreateTag_test.go | 81 ++- cmd/abapEnvironmentPullGitRepo.go | 106 +--- cmd/abapEnvironmentPullGitRepo_test.go | 86 +--- cmd/abapEnvironmentPushATCSystemConfig.go | 2 +- cmd/abapEnvironmentRunATCCheck.go | 2 +- pkg/abaputils/abaputils.go | 36 +- pkg/abaputils/abaputils_test.go | 8 +- pkg/abaputils/manageGitRepositoryUtils.go | 235 +-------- .../manageGitRepositoryUtils_test.go | 67 +-- pkg/abaputils/sap_com_0510.go | 369 +++++++++++++ pkg/abaputils/sap_com_0510_test.go | 483 ++++++++++++++++++ pkg/abaputils/softwareComponentApiManager.go | 194 +++++++ 17 files changed, 1471 insertions(+), 1056 deletions(-) create mode 100644 pkg/abaputils/sap_com_0510.go create mode 100644 pkg/abaputils/sap_com_0510_test.go create mode 100644 pkg/abaputils/softwareComponentApiManager.go diff --git a/cmd/abapEnvironmentCheckoutBranch.go b/cmd/abapEnvironmentCheckoutBranch.go index 8fcb26ead3..a1c7bd9642 100644 --- a/cmd/abapEnvironmentCheckoutBranch.go +++ b/cmd/abapEnvironmentCheckoutBranch.go @@ -1,10 +1,7 @@ package cmd import ( - "encoding/json" "fmt" - "io" - "net/http/cookiejar" "reflect" "time" @@ -28,49 +25,39 @@ func abapEnvironmentCheckoutBranch(options abapEnvironmentCheckoutBranchOptions, Exec: &c, } - client := piperhttp.Client{} + apiManager := abaputils.SoftwareComponentApiManager{ + Client: &piperhttp.Client{}, + PollIntervall: 5 * time.Second, + } // error situations should stop execution through log.Entry().Fatal() call which leads to an os.Exit(1) in the end - err := runAbapEnvironmentCheckoutBranch(&options, &autils, &client) + err := runAbapEnvironmentCheckoutBranch(&options, &autils, &apiManager) if err != nil { log.Entry().WithError(err).Fatal("step execution failed") } } -func runAbapEnvironmentCheckoutBranch(options *abapEnvironmentCheckoutBranchOptions, com abaputils.Communication, client piperhttp.Sender) (err error) { +func runAbapEnvironmentCheckoutBranch(options *abapEnvironmentCheckoutBranchOptions, com abaputils.Communication, apiManager abaputils.SoftwareComponentApiManagerInterface) (err error) { // Mapping for options subOptions := convertCheckoutConfig(options) // Determine the host, user and password, either via the input parameters or via a cloud foundry service key - connectionDetails, errorGetInfo := com.GetAbapCommunicationArrangementInfo(subOptions, "/sap/opu/odata/sap/MANAGE_GIT_REPOSITORY/") + connectionDetails, errorGetInfo := com.GetAbapCommunicationArrangementInfo(subOptions, "") if errorGetInfo != nil { log.Entry().WithError(errorGetInfo).Fatal("Parameters for the ABAP Connection not available") } - // Configuring the HTTP Client and CookieJar - cookieJar, errorCookieJar := cookiejar.New(nil) - if errorCookieJar != nil { - return errors.Wrap(errorCookieJar, "Could not create a Cookie Jar") - } - clientOptions := piperhttp.ClientOptions{ - MaxRequestDuration: 180 * time.Second, - CookieJar: cookieJar, - Username: connectionDetails.User, - Password: connectionDetails.Password, - } - client.SetOptions(clientOptions) - pollIntervall := com.GetPollIntervall() - repositories := []abaputils.Repository{} err = checkCheckoutBranchRepositoryConfiguration(*options) - - if err == nil { - repositories, err = abaputils.GetRepositories(&abaputils.RepositoriesConfig{BranchName: options.BranchName, RepositoryName: options.RepositoryName, Repositories: options.Repositories}, true) + if err != nil { + return errors.Wrap(err, "Configuration is not consistent") } - if err == nil { - err = checkoutBranches(repositories, connectionDetails, client, pollIntervall) + repositories, err = abaputils.GetRepositories(&abaputils.RepositoriesConfig{BranchName: options.BranchName, RepositoryName: options.RepositoryName, Repositories: options.Repositories}, true) + if err != nil { + return errors.Wrap(err, "Could not read repositories") } + err = checkoutBranches(repositories, connectionDetails, apiManager) if err != nil { return fmt.Errorf("Something failed during the checkout: %w", err) } @@ -79,10 +66,10 @@ func runAbapEnvironmentCheckoutBranch(options *abapEnvironmentCheckoutBranchOpti return nil } -func checkoutBranches(repositories []abaputils.Repository, checkoutConnectionDetails abaputils.ConnectionDetailsHTTP, client piperhttp.Sender, pollIntervall time.Duration) (err error) { +func checkoutBranches(repositories []abaputils.Repository, checkoutConnectionDetails abaputils.ConnectionDetailsHTTP, apiManager abaputils.SoftwareComponentApiManagerInterface) (err error) { log.Entry().Infof("Start switching %v branches", len(repositories)) for _, repo := range repositories { - err = handleCheckout(repo, checkoutConnectionDetails, client, pollIntervall) + err = handleCheckout(repo, checkoutConnectionDetails, apiManager) if err != nil { break } @@ -90,67 +77,9 @@ func checkoutBranches(repositories []abaputils.Repository, checkoutConnectionDet return err } -func triggerCheckout(repositoryName string, branchName string, checkoutConnectionDetails abaputils.ConnectionDetailsHTTP, client piperhttp.Sender) (abaputils.ConnectionDetailsHTTP, error) { - uriConnectionDetails := checkoutConnectionDetails - uriConnectionDetails.URL = "" - checkoutConnectionDetails.XCsrfToken = "fetch" - - if repositoryName == "" || branchName == "" { - return uriConnectionDetails, fmt.Errorf("Failed to trigger checkout: %w", errors.New("Repository and/or Branch Configuration is empty. Please make sure that you have specified the correct values")) - } - - // Loging into the ABAP System - getting the x-csrf-token and cookies - resp, err := abaputils.GetHTTPResponse("HEAD", checkoutConnectionDetails, nil, client) - if err != nil { - err = abaputils.HandleHTTPError(resp, err, "Authentication on the ABAP system failed", checkoutConnectionDetails) - return uriConnectionDetails, err - } - defer resp.Body.Close() - - log.Entry().WithField("StatusCode", resp.Status).WithField("ABAP Endpoint", checkoutConnectionDetails.URL).Debug("Authentication on the ABAP system was successful") - uriConnectionDetails.XCsrfToken = resp.Header.Get("X-Csrf-Token") - checkoutConnectionDetails.XCsrfToken = uriConnectionDetails.XCsrfToken - - // the request looks like: POST/sap/opu/odata/sap/MANAGE_GIT_REPOSITORY/checkout_branch?branch_name='newBranch'&sc_name=/DMO/GIT_REPOSITORY' - checkoutConnectionDetails.URL = checkoutConnectionDetails.URL + `/checkout_branch?branch_name='` + branchName + `'&sc_name='` + repositoryName + `'` - jsonBody := []byte(``) - - // no JSON body needed - resp, err = abaputils.GetHTTPResponse("POST", checkoutConnectionDetails, jsonBody, client) - if err != nil { - err = abaputils.HandleHTTPError(resp, err, "Could not trigger checkout of branch "+branchName, uriConnectionDetails) - return uriConnectionDetails, err - } - defer resp.Body.Close() - log.Entry().WithField("StatusCode", resp.StatusCode).WithField("repositoryName", repositoryName).WithField("branchName", branchName).Debug("Triggered checkout of branch") - - // Parse Response - var body abaputils.PullEntity - var abapResp map[string]*json.RawMessage - bodyText, errRead := io.ReadAll(resp.Body) - if errRead != nil { - return uriConnectionDetails, err - } - if err := json.Unmarshal(bodyText, &abapResp); err != nil { - return uriConnectionDetails, err - } - if err := json.Unmarshal(*abapResp["d"], &body); err != nil { - return uriConnectionDetails, err - } - - if reflect.DeepEqual(abaputils.PullEntity{}, body) { - log.Entry().WithField("StatusCode", resp.Status).WithField("branchName", branchName).Error("Could not switch to specified branch") - err := errors.New("Request to ABAP System failed") - return uriConnectionDetails, err - } - - uriConnectionDetails.URL = body.Metadata.URI - return uriConnectionDetails, nil -} - func checkCheckoutBranchRepositoryConfiguration(options abapEnvironmentCheckoutBranchOptions) error { if options.Repositories == "" && options.RepositoryName == "" && options.BranchName == "" { - return fmt.Errorf("Checking configuration failed: %w", errors.New("You have not specified any repository or branch configuration to be checked out in the ABAP Environment System. Please make sure that you specified the repositories with their branches that should be checked out either in a dedicated file or via the parameters 'repositoryName' and 'branchName'. For more information please read the User documentation")) + return errors.New("You have not specified any repository or branch configuration to be checked out in the ABAP Environment System. Please make sure that you specified the repositories with their branches that should be checked out either in a dedicated file or via the parameters 'repositoryName' and 'branchName'. For more information please read the user documentation") } if options.Repositories != "" && options.RepositoryName != "" && options.BranchName != "" { log.Entry().Info("It seems like you have specified repositories directly via the configuration parameters 'repositoryName' and 'branchName' as well as in the dedicated repositories configuration file. Please note that in this case both configurations will be handled and checked out.") @@ -166,20 +95,26 @@ func checkCheckoutBranchRepositoryConfiguration(options abapEnvironmentCheckoutB return nil } -func handleCheckout(repo abaputils.Repository, checkoutConnectionDetails abaputils.ConnectionDetailsHTTP, client piperhttp.Sender, pollIntervall time.Duration) (err error) { +func handleCheckout(repo abaputils.Repository, checkoutConnectionDetails abaputils.ConnectionDetailsHTTP, apiManager abaputils.SoftwareComponentApiManagerInterface) (err error) { + if reflect.DeepEqual(abaputils.Repository{}, repo) { return fmt.Errorf("Failed to read repository configuration: %w", errors.New("Error in configuration, most likely you have entered empty or wrong configuration values. Please make sure that you have correctly specified the branches in the repositories to be checked out")) } startCheckoutLogs(repo.Branch, repo.Name) - uriConnectionDetails, err := triggerCheckout(repo.Name, repo.Branch, checkoutConnectionDetails, client) + api, errGetAPI := apiManager.GetAPI(checkoutConnectionDetails, repo) + if errGetAPI != nil { + return errors.Wrap(errGetAPI, "Could not initialize the connection to the system") + } + + err = api.CheckoutBranch() if err != nil { return fmt.Errorf("Failed to trigger Checkout: %w", errors.New("Checkout of "+repo.Branch+" for software component "+repo.Name+" failed on the ABAP System")) } // Polling the status of the repository import on the ABAP Environment system - status, err := abaputils.PollEntity(repo.Name, uriConnectionDetails, client, pollIntervall) - if err != nil { + status, errorPollEntity := abaputils.PollEntity(api, apiManager.GetPollIntervall()) + if errorPollEntity != nil { return fmt.Errorf("Failed to poll Checkout: %w", errors.New("Status of checkout action on repository"+repo.Name+" failed on the ABAP System")) } const abapStatusCheckoutFail = "E" diff --git a/cmd/abapEnvironmentCheckoutBranch_test.go b/cmd/abapEnvironmentCheckoutBranch_test.go index 91bbe3cf62..5ba5334a8a 100644 --- a/cmd/abapEnvironmentCheckoutBranch_test.go +++ b/cmd/abapEnvironmentCheckoutBranch_test.go @@ -7,9 +7,9 @@ import ( "encoding/json" "os" "testing" + "time" "github.com/SAP/jenkins-library/pkg/abaputils" - "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) @@ -67,11 +67,12 @@ func TestCheckoutBranchStep(t *testing.T) { StatusCode: 200, } - err := runAbapEnvironmentCheckoutBranch(&config, &autils, client) + apiManager = &abaputils.SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + err := runAbapEnvironmentCheckoutBranch(&config, &autils, apiManager) assert.NoError(t, err, "Did not expect error") }) t.Run("Run Step Failure - empty config", func(t *testing.T) { - expectedErrorMessage := "Something failed during the checkout: Checking configuration failed: You have not specified any repository or branch configuration to be checked out in the ABAP Environment System. Please make sure that you specified the repositories with their branches that should be checked out either in a dedicated file or via the parameters 'repositoryName' and 'branchName'. For more information please read the User documentation" + expectedErrorMessage := "Configuration is not consistent: You have not specified any repository or branch configuration to be checked out in the ABAP Environment System. Please make sure that you specified the repositories with their branches that should be checked out either in a dedicated file or via the parameters 'repositoryName' and 'branchName'. For more information please read the user documentation" var autils = abaputils.AUtilsMock{} defer autils.Cleanup() @@ -85,7 +86,6 @@ func TestCheckoutBranchStep(t *testing.T) { logResultError := `{"d": { "sc_name": "/DMO/SWC", "status": "S", "to_Log_Overview": { "results": [ { "log_index": 1, "log_name": "Main Import", "type_of_found_issues": "Error", "timestamp": "/Date(1644332299000+0000)/", "to_Log_Protocol": { "results": [ { "log_index": 1, "index_no": "1", "log_name": "", "type": "Info", "descr": "Main import", "timestamp": null, "criticality": 0 } ] } } ] } } }` client := &abaputils.ClientMock{ BodyList: []string{ - `{"d" : [] }`, `{"d" : ` + executionLogStringCheckout + `}`, logResultError, `{"d" : { "status" : "E" } }`, @@ -96,7 +96,8 @@ func TestCheckoutBranchStep(t *testing.T) { StatusCode: 200, } - err := runAbapEnvironmentCheckoutBranch(&config, &autils, client) + apiManager = &abaputils.SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + err := runAbapEnvironmentCheckoutBranch(&config, &autils, apiManager) assert.EqualError(t, err, expectedErrorMessage) }) t.Run("Run Step Failure - wrong status", func(t *testing.T) { @@ -124,7 +125,6 @@ func TestCheckoutBranchStep(t *testing.T) { logResultError := `{"d": { "sc_name": "/DMO/SWC", "status": "S", "to_Log_Overview": { "results": [ { "log_index": 1, "log_name": "Main Import", "type_of_found_issues": "Error", "timestamp": "/Date(1644332299000+0000)/", "to_Log_Protocol": { "results": [ { "log_index": 1, "index_no": "1", "log_name": "", "type": "Info", "descr": "Main import", "timestamp": null, "criticality": 0 } ] } } ] } } }` client := &abaputils.ClientMock{ BodyList: []string{ - `{"d" : [] }`, `{"d" : ` + executionLogStringCheckout + `}`, logResultError, `{"d" : { "status" : "E" } }`, @@ -135,7 +135,8 @@ func TestCheckoutBranchStep(t *testing.T) { StatusCode: 200, } - err := runAbapEnvironmentCheckoutBranch(&config, &autils, client) + apiManager = &abaputils.SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + err := runAbapEnvironmentCheckoutBranch(&config, &autils, apiManager) assert.EqualError(t, err, expectedErrorMessage) }) t.Run("Success case: checkout Branches from file config", func(t *testing.T) { @@ -183,11 +184,12 @@ repositories: Password: "testPassword", Repositories: "repositoriesTest.yml", } - err = runAbapEnvironmentCheckoutBranch(&config, &autils, client) + apiManager = &abaputils.SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + err = runAbapEnvironmentCheckoutBranch(&config, &autils, apiManager) assert.NoError(t, err) }) t.Run("Failure case: checkout Branches from empty file config", func(t *testing.T) { - expectedErrorMessage := "Something failed during the checkout: Error in config file repositoriesTest.yml, AddonDescriptor doesn't contain any repositories" + expectedErrorMessage := "Could not read repositories: Error in config file repositoriesTest.yml, AddonDescriptor doesn't contain any repositories" var autils = abaputils.AUtilsMock{} defer autils.Cleanup() @@ -226,11 +228,12 @@ repositories: Password: "testPassword", Repositories: "repositoriesTest.yml", } - err = runAbapEnvironmentCheckoutBranch(&config, &autils, client) + apiManager = &abaputils.SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + err = runAbapEnvironmentCheckoutBranch(&config, &autils, apiManager) assert.EqualError(t, err, expectedErrorMessage) }) t.Run("Failure case: checkout Branches from wrong file config", func(t *testing.T) { - expectedErrorMessage := "Something failed during the checkout: Could not unmarshal repositoriesTest.yml" + expectedErrorMessage := "Could not read repositories: Could not unmarshal repositoriesTest.yml" var autils = abaputils.AUtilsMock{} defer autils.Cleanup() @@ -274,88 +277,12 @@ repositories: Password: "testPassword", Repositories: "repositoriesTest.yml", } - err = runAbapEnvironmentCheckoutBranch(&config, &autils, client) + apiManager = &abaputils.SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + err = runAbapEnvironmentCheckoutBranch(&config, &autils, apiManager) assert.EqualError(t, err, expectedErrorMessage) }) } -func TestTriggerCheckout(t *testing.T) { - t.Run("Test trigger checkout: success case", func(t *testing.T) { - - // given - receivedURI := "example.com/Branches" - uriExpected := receivedURI - tokenExpected := "myToken" - - client := &abaputils.ClientMock{ - Body: `{"d" : { "__metadata" : { "uri" : "` + receivedURI + `" } } }`, - Token: tokenExpected, - StatusCode: 200, - } - config := abapEnvironmentCheckoutBranchOptions{ - CfAPIEndpoint: "https://api.endpoint.com", - CfOrg: "testOrg", - CfSpace: "testSpace", - CfServiceInstance: "testInstance", - CfServiceKeyName: "testServiceKey", - Username: "testUser", - Password: "testPassword", - RepositoryName: "testRepo1", - BranchName: "feature-unit-test", - } - con := abaputils.ConnectionDetailsHTTP{ - User: "MY_USER", - Password: "MY_PW", - URL: "https://api.endpoint.com/Branches", - } - // when - entityConnection, err := triggerCheckout(config.RepositoryName, config.BranchName, con, client) - - // then - assert.NoError(t, err) - assert.Equal(t, uriExpected, entityConnection.URL) - assert.Equal(t, tokenExpected, entityConnection.XCsrfToken) - }) - - t.Run("Test trigger checkout: ABAP Error case", func(t *testing.T) { - - // given - errorMessage := "ABAP Error Message" - errorCode := "ERROR/001" - HTTPErrorMessage := "HTTP Error Message" - combinedErrorMessage := "HTTP Error Message: ERROR/001 - ABAP Error Message" - - client := &abaputils.ClientMock{ - Body: `{"error" : { "code" : "` + errorCode + `", "message" : { "lang" : "en", "value" : "` + errorMessage + `" } } }`, - Token: "myToken", - StatusCode: 400, - Error: errors.New(HTTPErrorMessage), - } - config := abapEnvironmentCheckoutBranchOptions{ - CfAPIEndpoint: "https://api.endpoint.com", - CfOrg: "testOrg", - CfSpace: "testSpace", - CfServiceInstance: "testInstance", - CfServiceKeyName: "testServiceKey", - Username: "testUser", - Password: "testPassword", - RepositoryName: "testRepo1", - BranchName: "feature-unit-test", - } - con := abaputils.ConnectionDetailsHTTP{ - User: "MY_USER", - Password: "MY_PW", - URL: "https://api.endpoint.com/Branches", - } - - // when - _, err := triggerCheckout(config.RepositoryName, config.BranchName, con, client) - - // then - assert.Equal(t, combinedErrorMessage, err.Error(), "Different error message expected") - }) -} - func TestCheckoutConfigChecker(t *testing.T) { t.Run("Success case: check config", func(t *testing.T) { config := abapEnvironmentCheckoutBranchOptions{ @@ -374,7 +301,7 @@ func TestCheckoutConfigChecker(t *testing.T) { assert.NoError(t, err) }) t.Run("Failure case: check empty config", func(t *testing.T) { - expectedErrorMessage := "Checking configuration failed: You have not specified any repository or branch configuration to be checked out in the ABAP Environment System. Please make sure that you specified the repositories with their branches that should be checked out either in a dedicated file or via the parameters 'repositoryName' and 'branchName'. For more information please read the User documentation" + expectedErrorMessage := "You have not specified any repository or branch configuration to be checked out in the ABAP Environment System. Please make sure that you specified the repositories with their branches that should be checked out either in a dedicated file or via the parameters 'repositoryName' and 'branchName'. For more information please read the user documentation" config := abapEnvironmentCheckoutBranchOptions{} err := checkCheckoutBranchRepositoryConfiguration(config) diff --git a/cmd/abapEnvironmentCloneGitRepo.go b/cmd/abapEnvironmentCloneGitRepo.go index 7b6db9875d..9776312b2e 100644 --- a/cmd/abapEnvironmentCloneGitRepo.go +++ b/cmd/abapEnvironmentCloneGitRepo.go @@ -1,12 +1,6 @@ package cmd import ( - "encoding/json" - "fmt" - "io" - "net/http" - "net/http/cookiejar" - "reflect" "time" "github.com/SAP/jenkins-library/pkg/abaputils" @@ -28,81 +22,138 @@ func abapEnvironmentCloneGitRepo(config abapEnvironmentCloneGitRepoOptions, _ *t Exec: &c, } - client := piperhttp.Client{} + apiManager := abaputils.SoftwareComponentApiManager{ + Client: &piperhttp.Client{}, + PollIntervall: 5 * time.Second, + } // error situations should stop execution through log.Entry().Fatal() call which leads to an os.Exit(1) in the end - err := runAbapEnvironmentCloneGitRepo(&config, &autils, &client) + err := runAbapEnvironmentCloneGitRepo(&config, &autils, &apiManager) if err != nil { log.Entry().WithError(err).Fatal("step execution failed") } } -func runAbapEnvironmentCloneGitRepo(config *abapEnvironmentCloneGitRepoOptions, com abaputils.Communication, client piperhttp.Sender) error { +func runAbapEnvironmentCloneGitRepo(config *abapEnvironmentCloneGitRepoOptions, com abaputils.Communication, apiManager abaputils.SoftwareComponentApiManagerInterface) error { // Mapping for options subOptions := convertCloneConfig(config) + errConfig := checkConfiguration(config) + if errConfig != nil { + return errors.Wrap(errConfig, "The provided configuration is not allowed") + } + + repositories, errGetRepos := abaputils.GetRepositories(&abaputils.RepositoriesConfig{BranchName: config.BranchName, RepositoryName: config.RepositoryName, Repositories: config.Repositories}, false) + if errGetRepos != nil { + return errors.Wrap(errGetRepos, "Could not read repositories") + } + // Determine the host, user and password, either via the input parameters or via a cloud foundry service key connectionDetails, errorGetInfo := com.GetAbapCommunicationArrangementInfo(subOptions, "") if errorGetInfo != nil { return errors.Wrap(errorGetInfo, "Parameters for the ABAP Connection not available") } - // Configuring the HTTP Client and CookieJar - cookieJar, errorCookieJar := cookiejar.New(nil) - if errorCookieJar != nil { - return errors.Wrap(errorCookieJar, "Could not create a Cookie Jar") - } - - client.SetOptions(piperhttp.ClientOptions{ - MaxRequestDuration: 180 * time.Second, - CookieJar: cookieJar, - Username: connectionDetails.User, - Password: connectionDetails.Password, - }) + log.Entry().Infof("Start cloning %v repositories", len(repositories)) + for _, repo := range repositories { - errConfig := checkConfiguration(config) - if errConfig != nil { - return errors.Wrap(errConfig, "The provided configuration is not allowed") + cloneError := cloneSingleRepo(apiManager, connectionDetails, repo, config, com) + if cloneError != nil { + return cloneError + } } + abaputils.AddDefaultDashedLine(1) + log.Entry().Info("All repositories were cloned successfully") + return nil +} - repositories, errGetRepos := abaputils.GetRepositories(&abaputils.RepositoriesConfig{BranchName: config.BranchName, RepositoryName: config.RepositoryName, Repositories: config.Repositories}, true) - if errGetRepos != nil { - return fmt.Errorf("Something failed during the clone: %w", errGetRepos) +func cloneSingleRepo(apiManager abaputils.SoftwareComponentApiManagerInterface, connectionDetails abaputils.ConnectionDetailsHTTP, repo abaputils.Repository, config *abapEnvironmentCloneGitRepoOptions, com abaputils.Communication) error { + + // New API instance for each request + // Triggering the Clone of the repository into the ABAP Environment system + // Polling the status of the repository import on the ABAP Environment system + // If the repository had been cloned already, as checkout/pull has been done - polling the status is not necessary anymore + api, errGetAPI := apiManager.GetAPI(connectionDetails, repo) + if errGetAPI != nil { + return errors.Wrap(errGetAPI, "Could not initialize the connection to the system") } - log.Entry().Infof("Start cloning %v repositories", len(repositories)) - for _, repo := range repositories { + logString := repo.GetCloneLogString() + errorString := "Clone of " + logString + " failed on the ABAP system" - logString := repo.GetCloneLogString() - errorString := "Clone of " + logString + " failed on the ABAP system" + abaputils.AddDefaultDashedLine(1) + log.Entry().Info("Start cloning " + logString) + abaputils.AddDefaultDashedLine(1) - abaputils.AddDefaultDashedLine() - log.Entry().Info("Start cloning " + logString) - abaputils.AddDefaultDashedLine() + alreadyCloned, activeBranch, errCheckCloned := api.GetRepository() + if errCheckCloned != nil { + return errors.Wrapf(errCheckCloned, errorString) + } - // Triggering the Clone of the repository into the ABAP Environment system - uriConnectionDetails, errorTriggerClone, didCheckoutPullInstead := triggerClone(repo, connectionDetails, client) - if errorTriggerClone != nil { - return errors.Wrapf(errorTriggerClone, errorString) + if !alreadyCloned { + errClone := api.Clone() + if errClone != nil { + return errors.Wrapf(errClone, errorString) } - if !didCheckoutPullInstead { - // Polling the status of the repository import on the ABAP Environment system - // If the repository had been cloned already, as checkout/pull has been done - polling the status is not necessary anymore - status, errorPollEntity := abaputils.PollEntity(repo.Name, uriConnectionDetails, client, com.GetPollIntervall()) - if errorPollEntity != nil { - return errors.Wrapf(errorPollEntity, errorString) - } - if status == "E" { - return errors.New("Clone of " + logString + " failed on the ABAP System") + status, errorPollEntity := abaputils.PollEntity(api, apiManager.GetPollIntervall()) + if errorPollEntity != nil { + return errors.Wrapf(errorPollEntity, errorString) + } + if status == "E" { + return errors.New("Clone of " + logString + " failed on the ABAP System") + } + log.Entry().Info("The " + logString + " was cloned successfully") + } else { + abaputils.AddDefaultDashedLine(2) + log.Entry().Infof("%s", "The repository / software component has already been cloned on the ABAP Environment system ") + log.Entry().Infof("%s", "If required, a `checkout branch`, and a `pull` will be performed instead") + abaputils.AddDefaultDashedLine(2) + var returnedError error + if repo.Branch != "" && !(activeBranch == repo.Branch) { + returnedError = runAbapEnvironmentCheckoutBranch(getCheckoutOptions(config, repo), com, apiManager) + abaputils.AddDefaultDashedLine(2) + if returnedError != nil { + return returnedError } - log.Entry().Info("The " + logString + " was cloned successfully") } + returnedError = runAbapEnvironmentPullGitRepo(getPullOptions(config, repo), com, apiManager) + return returnedError } - abaputils.AddDefaultDashedLine() - log.Entry().Info("All repositories were cloned successfully") return nil } +func getCheckoutOptions(config *abapEnvironmentCloneGitRepoOptions, repo abaputils.Repository) *abapEnvironmentCheckoutBranchOptions { + checkoutOptions := abapEnvironmentCheckoutBranchOptions{ + Username: config.Username, + Password: config.Password, + Host: config.Host, + RepositoryName: repo.Name, + BranchName: repo.Branch, + CfAPIEndpoint: config.CfAPIEndpoint, + CfOrg: config.CfOrg, + CfServiceInstance: config.CfServiceInstance, + CfServiceKeyName: config.CfServiceKeyName, + CfSpace: config.CfSpace, + } + return &checkoutOptions +} + +func getPullOptions(config *abapEnvironmentCloneGitRepoOptions, repo abaputils.Repository) *abapEnvironmentPullGitRepoOptions { + pullOptions := abapEnvironmentPullGitRepoOptions{ + Username: config.Username, + Password: config.Password, + Host: config.Host, + RepositoryName: repo.Name, + CommitID: repo.CommitID, + CfAPIEndpoint: config.CfAPIEndpoint, + CfOrg: config.CfOrg, + CfServiceInstance: config.CfServiceInstance, + CfServiceKeyName: config.CfServiceKeyName, + CfSpace: config.CfSpace, + } + return &pullOptions +} + func checkConfiguration(config *abapEnvironmentCloneGitRepoOptions) error { if config.Repositories != "" && config.RepositoryName != "" { return errors.New("It is not allowed to configure the parameters `repositories`and `repositoryName` at the same time") @@ -113,125 +164,14 @@ func checkConfiguration(config *abapEnvironmentCloneGitRepoOptions) error { return nil } -func triggerClone(repo abaputils.Repository, cloneConnectionDetails abaputils.ConnectionDetailsHTTP, client piperhttp.Sender) (abaputils.ConnectionDetailsHTTP, error, bool) { +func triggerClone(repo abaputils.Repository, api abaputils.SoftwareComponentApiInterface) (error, bool) { - uriConnectionDetails := cloneConnectionDetails - cloneConnectionDetails.XCsrfToken = "fetch" - - cloneConnectionDetails.URL = cloneConnectionDetails.URL + "/sap/opu/odata/sap/MANAGE_GIT_REPOSITORY/Clones" - - // Loging into the ABAP System - getting the x-csrf-token and cookies - resp, err := abaputils.GetHTTPResponse("HEAD", cloneConnectionDetails, nil, client) - if err != nil { - err = abaputils.HandleHTTPError(resp, err, "Authentication on the ABAP system failed", cloneConnectionDetails) - return uriConnectionDetails, err, false - } - defer resp.Body.Close() - - log.Entry().WithField("StatusCode", resp.Status).WithField("ABAP Endpoint", cloneConnectionDetails.URL).Debug("Authentication on the ABAP system successful") - uriConnectionDetails.XCsrfToken = resp.Header.Get("X-Csrf-Token") - cloneConnectionDetails.XCsrfToken = uriConnectionDetails.XCsrfToken - - // Trigger the Clone of a Repository - if repo.Name == "" { - return uriConnectionDetails, errors.New("An empty string was passed for the parameter 'repositoryName'"), false - } - - jsonBody := []byte(repo.GetCloneRequestBody()) - resp, err = abaputils.GetHTTPResponse("POST", cloneConnectionDetails, jsonBody, client) - if err != nil { - err, alreadyCloned := handleCloneError(resp, err, cloneConnectionDetails, client, repo) - return uriConnectionDetails, err, alreadyCloned - } - defer resp.Body.Close() - log.Entry().WithField("StatusCode", resp.Status).WithField("repositoryName", repo.Name).WithField("branchName", repo.Branch).WithField("commitID", repo.CommitID).WithField("Tag", repo.Tag).Info("Triggered Clone of Repository / Software Component") - - // Parse Response - var body abaputils.CloneEntity - var abapResp map[string]*json.RawMessage - bodyText, errRead := io.ReadAll(resp.Body) - if errRead != nil { - return uriConnectionDetails, err, false - } - if err := json.Unmarshal(bodyText, &abapResp); err != nil { - return uriConnectionDetails, err, false - } - if err := json.Unmarshal(*abapResp["d"], &body); err != nil { - return uriConnectionDetails, err, false - } - if reflect.DeepEqual(abaputils.CloneEntity{}, body) { - log.Entry().WithField("StatusCode", resp.Status).WithField("repositoryName", repo.Name).WithField("branchName", repo.Branch).WithField("commitID", repo.CommitID).WithField("Tag", repo.Tag).Error("Could not Clone the Repository / Software Component") - err := errors.New("Request to ABAP System not successful") - return uriConnectionDetails, err, false - } + //cloneConnectionDetails.URL = cloneConnectionDetails.URL + "/sap/opu/odata/sap/MANAGE_GIT_REPOSITORY/Clones" // The entity "Clones" does not allow for polling. To poll the progress, the related entity "Pull" has to be called // While "Clones" has the key fields UUID, SC_NAME and BRANCH_NAME, "Pull" only has the key field UUID - uriConnectionDetails.URL = uriConnectionDetails.URL + "/sap/opu/odata/sap/MANAGE_GIT_REPOSITORY/Pull(uuid=guid'" + body.UUID + "')" - return uriConnectionDetails, nil, false -} - -func handleCloneError(resp *http.Response, err error, cloneConnectionDetails abaputils.ConnectionDetailsHTTP, client piperhttp.Sender, repo abaputils.Repository) (returnedError error, alreadyCloned bool) { - alreadyCloned = false - returnedError = nil - if resp == nil { - log.Entry().WithError(err).WithField("ABAP Endpoint", cloneConnectionDetails.URL).Error("Request failed") - returnedError = errors.New("Response is nil") - return - } - defer resp.Body.Close() - errorText, errorCode, parsingError := abaputils.GetErrorDetailsFromResponse(resp) - if parsingError != nil { - returnedError = err - return - } - if errorCode == "A4C_A2G/257" { - // With the latest release, a repeated "clone" was prohibited - // As an intermediate workaround, we react to the error message A4C_A2G/257 that gets thrown, if the repository had already been cloned - // In this case, a checkout branch and a pull will be performed - alreadyCloned = true - abaputils.AddDefaultDashedLine() - abaputils.AddDefaultDashedLine() - log.Entry().Infof("%s", "The repository / software component has already been cloned on the ABAP Environment system ") - log.Entry().Infof("%s", "A `checkout branch` and a `pull` will be performed instead") - abaputils.AddDefaultDashedLine() - abaputils.AddDefaultDashedLine() - checkoutOptions := abapEnvironmentCheckoutBranchOptions{ - Username: cloneConnectionDetails.User, - Password: cloneConnectionDetails.Password, - Host: cloneConnectionDetails.Host, - RepositoryName: repo.Name, - BranchName: repo.Branch, - } - c := command.Command{} - c.Stdout(log.Writer()) - c.Stderr(log.Writer()) - com := abaputils.AbapUtils{ - Exec: &c, - } - returnedError = runAbapEnvironmentCheckoutBranch(&checkoutOptions, &com, client) - if returnedError != nil { - return - } - abaputils.AddDefaultDashedLine() - abaputils.AddDefaultDashedLine() - pullOptions := abapEnvironmentPullGitRepoOptions{ - Username: cloneConnectionDetails.User, - Password: cloneConnectionDetails.Password, - Host: cloneConnectionDetails.Host, - RepositoryName: repo.Name, - CommitID: repo.CommitID, - } - returnedError = runAbapEnvironmentPullGitRepo(&pullOptions, &com, client) - if returnedError != nil { - return - } - } else { - log.Entry().WithField("StatusCode", resp.Status).Error("Could not clone the " + repo.GetCloneLogString()) - abapError := errors.New(fmt.Sprintf("%s - %s", errorCode, errorText)) - returnedError = errors.Wrap(abapError, err.Error()) - } - return + //uriConnectionDetails.URL = uriConnectionDetails.URL + "/sap/opu/odata/sap/MANAGE_GIT_REPOSITORY/Pull(uuid=guid'" + body.UUID + "')" + return nil, false } func convertCloneConfig(config *abapEnvironmentCloneGitRepoOptions) abaputils.AbapEnvironmentOptions { diff --git a/cmd/abapEnvironmentCloneGitRepo_test.go b/cmd/abapEnvironmentCloneGitRepo_test.go index b525a73e34..f009b831f4 100644 --- a/cmd/abapEnvironmentCloneGitRepo_test.go +++ b/cmd/abapEnvironmentCloneGitRepo_test.go @@ -4,19 +4,17 @@ package cmd import ( - "bytes" "encoding/json" - "io" - "net/http" "os" "testing" + "time" "github.com/SAP/jenkins-library/pkg/abaputils" - "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) var executionLogStringClone string +var apiManager abaputils.SoftwareComponentApiManagerInterface func init() { executionLog := abaputils.LogProtocolResults{ @@ -29,9 +27,11 @@ func init() { Timestamp: "/Date(1644332299000+0000)/", }, }, + Count: "1", } executionLogResponse, _ := json.Marshal(executionLog) executionLogStringClone = string(executionLogResponse) + } func TestCloneStep(t *testing.T) { @@ -80,13 +80,13 @@ repositories: logResultSuccess := `{"d": { "sc_name": "/DMO/SWC", "status": "S", "to_Log_Overview": { "results": [ { "log_index": 1, "log_name": "Main Import", "type_of_found_issues": "Success", "timestamp": "/Date(1644332299000+0000)/", "to_Log_Protocol": { "results": [ { "log_index": 1, "index_no": "1", "log_name": "", "type": "Info", "descr": "Main import", "timestamp": null, "criticality": 0 } ] } } ] } } }` client := &abaputils.ClientMock{ BodyList: []string{ - `{"d" : [] }`, `{"d" : ` + executionLogStringClone + `}`, logResultSuccess, `{"d" : { "status" : "S" } }`, `{"d" : { "status" : "R" } }`, `{"d" : { "status" : "R" } }`, `{"d" : { "status" : "R" } }`, + `{"d" : { "sc_name" : "/DMO/REPO_B", "avail_on_instance" : false, "active_branch": "branchB" } }`, `{"d" : [] }`, `{"d" : ` + executionLogStringClone + `}`, logResultSuccess, @@ -94,11 +94,14 @@ repositories: `{"d" : { "status" : "R" } }`, `{"d" : { "status" : "R" } }`, `{"d" : { "status" : "R" } }`, + `{"d" : { "sc_name" : "/DMO/REPO_A", "avail_on_instance" : true, "active_branch": "branchA" } }`, + `{"d" : [] }`, }, Token: "myToken", } - err = runAbapEnvironmentCloneGitRepo(&config, &autils, client) + apiManager = &abaputils.SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Nanosecond} + err = runAbapEnvironmentCloneGitRepo(&config, &autils, apiManager) assert.NoError(t, err, "Did not expect error") assert.Equal(t, 0, len(client.BodyList), "Not all requests were done") }) @@ -120,24 +123,25 @@ repositories: Username: "testUser", Password: "testPassword", RepositoryName: "testRepo1", - BranchName: "testBranch1", } - logResultSuccess := `{"d": { "sc_name": "/DMO/SWC", "status": "S", "to_Log_Overview": { "results": [ { "log_index": 1, "log_name": "Main Import", "type_of_found_issues": "Success", "timestamp": "/Date(1644332299000+0000)/", "to_Log_Protocol": { "results": [ { "log_index": 1, "index_no": "1", "log_name": "", "type": "Info", "descr": "Main import", "timestamp": null, "criticality": 0 } ] } } ] } } }` + logResultSuccess := `{"d": { "sc_name": "testRepo1", "status": "S", "to_Log_Overview": { "results": [ { "log_index": 1, "log_name": "Main Import", "type_of_found_issues": "Success", "timestamp": "/Date(1644332299000+0000)/", "to_Log_Protocol": { "results": [ { "log_index": 1, "index_no": "1", "log_name": "", "type": "Info", "descr": "Main import", "timestamp": null, "criticality": 0 } ] } } ] } } }` client := &abaputils.ClientMock{ BodyList: []string{ - `{"d" : [] }`, `{"d" : ` + executionLogStringClone + `}`, logResultSuccess, `{"d" : { "status" : "S" } }`, `{"d" : { "status" : "R" } }`, `{"d" : { "status" : "R" } }`, + `{"d" : { "sc_name" : "testRepo1", "avail_on_instance" : false, "active_branch": "testBranch1" } }`, + `{"d" : [] }`, }, Token: "myToken", StatusCode: 200, } - err := runAbapEnvironmentCloneGitRepo(&config, &autils, client) + apiManager = &abaputils.SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Nanosecond} + err := runAbapEnvironmentCloneGitRepo(&config, &autils, apiManager) assert.NoError(t, err, "Did not expect error") assert.Equal(t, 0, len(client.BodyList), "Not all requests were done") }) @@ -166,12 +170,15 @@ repositories: BodyList: []string{ `{"d" : {} }`, `{"d" : { "status" : "R" } }`, + `{"d" : { "sc_name" : "testRepo1", "avail_on_instance" : true, "active_branch": "testBranch1" } }`, + `{"d" : [] }`, }, Token: "myToken", StatusCode: 200, } - err := runAbapEnvironmentCloneGitRepo(&config, &autils, client) + apiManager = &abaputils.SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Nanosecond} + err := runAbapEnvironmentCloneGitRepo(&config, &autils, apiManager) if assert.Error(t, err, "Expected error") { assert.Equal(t, "Clone of repository / software component 'testRepo1', branch 'testBranch1' failed on the ABAP system: Request to ABAP System not successful", err.Error(), "Expected different error message") } @@ -232,10 +239,10 @@ repositories: Token: "myToken", StatusCode: 200, } - - err = runAbapEnvironmentCloneGitRepo(&config, &autils, client) + apiManager = &abaputils.SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Nanosecond} + err = runAbapEnvironmentCloneGitRepo(&config, &autils, apiManager) if assert.Error(t, err, "Expected error") { - assert.Equal(t, "Clone of repository / software component '/DMO/REPO_A', branch 'branchA', commit 'ABCD1234' failed on the ABAP System", err.Error(), "Expected different error message") + assert.Equal(t, "Clone of repository / software component '/DMO/REPO_A', branch 'branchA', commit 'ABCD1234' failed on the ABAP system: Request to ABAP System not successful", err.Error(), "Expected different error message") } }) @@ -268,8 +275,8 @@ repositories: Token: "myToken", StatusCode: 200, } - - err := runAbapEnvironmentCloneGitRepo(&config, &autils, client) + apiManager = &abaputils.SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Nanosecond} + err := runAbapEnvironmentCloneGitRepo(&config, &autils, apiManager) if assert.Error(t, err, "Expected error") { assert.Equal(t, "Clone of repository / software component 'testRepo1', branch 'testBranch1' failed on the ABAP system: Request to ABAP System not successful", err.Error(), "Expected different error message") } @@ -303,8 +310,8 @@ repositories: Token: "myToken", StatusCode: 200, } - - err := runAbapEnvironmentCloneGitRepo(&config, &autils, client) + apiManager = &abaputils.SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Nanosecond} + err := runAbapEnvironmentCloneGitRepo(&config, &autils, apiManager) if assert.Error(t, err, "Expected error") { assert.Equal(t, "Clone of repository / software component 'testRepo1', branch 'testBranch1' failed on the ABAP system: Request to ABAP System not successful", err.Error(), "Expected different error message") } @@ -337,10 +344,10 @@ repositories: Token: "myToken", StatusCode: 200, } - - err := runAbapEnvironmentCloneGitRepo(&config, &autils, client) + apiManager = &abaputils.SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Nanosecond} + err := runAbapEnvironmentCloneGitRepo(&config, &autils, apiManager) if assert.Error(t, err, "Expected error") { - assert.Equal(t, "Something failed during the clone: Could not find filename.yaml", err.Error(), "Expected different error message") + assert.Equal(t, "Could not read repositories: Could not find filename.yaml", err.Error(), "Expected different error message") } }) @@ -378,8 +385,8 @@ repositories: Token: "myToken", StatusCode: 200, } - - err := runAbapEnvironmentCloneGitRepo(&config, &autils, client) + apiManager = &abaputils.SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Nanosecond} + err := runAbapEnvironmentCloneGitRepo(&config, &autils, apiManager) if assert.Error(t, err, "Expected error") { assert.Equal(t, "The provided configuration is not allowed: It is not allowed to configure the parameters `repositories`and `repositoryName` at the same time", err.Error(), "Expected different error message") } @@ -387,7 +394,7 @@ repositories: } func TestALreadyCloned(t *testing.T) { - t.Run("Already Cloned", func(t *testing.T) { + t.Run("Already cloned, switch branch and pull instead", func(t *testing.T) { var autils = abaputils.AUtilsMock{} defer autils.Cleanup() @@ -396,46 +403,50 @@ func TestALreadyCloned(t *testing.T) { autils.ReturnedConnectionDetailsHTTP.URL = "https://example.com" autils.ReturnedConnectionDetailsHTTP.Host = "example.com" autils.ReturnedConnectionDetailsHTTP.XCsrfToken = "xcsrftoken" + + config := abapEnvironmentCloneGitRepoOptions{ + CfAPIEndpoint: "https://api.endpoint.com", + CfOrg: "testOrg", + CfSpace: "testSpace", + CfServiceInstance: "testInstance", + CfServiceKeyName: "testServiceKey", + Username: "testUser", + Password: "testPassword", + } + logResultSuccess := `{"d": { "sc_name": "/DMO/SWC", "status": "S", "to_Log_Overview": { "results": [ { "log_index": 1, "log_name": "Main Import", "type_of_found_issues": "Success", "timestamp": "/Date(1644332299000+0000)/", "to_Log_Protocol": { "results": [ { "log_index": 1, "index_no": "1", "log_name": "", "type": "Info", "descr": "Main import", "timestamp": null, "criticality": 0 } ] } } ] } } }` client := &abaputils.ClientMock{ BodyList: []string{ - `{"d" : }`, `{"d" : ` + executionLogStringClone + `}`, logResultSuccess, `{"d" : { "status" : "S" } }`, `{"d" : { "status" : "R" } }`, `{"d" : { "status" : "R" } }`, - `{"d" : }`, + `{"d" : [] }`, `{"d" : ` + executionLogStringClone + `}`, logResultSuccess, `{"d" : { "status" : "S" } }`, `{"d" : { "status" : "R" } }`, `{"d" : { "status" : "R" } }`, + `{"d" : { "sc_name" : "testRepo1", "avail_on_inst" : true, "active_branch": "testBranch1" } }`, + `{"d" : [] }`, }, Token: "myToken", StatusCode: 200, } - bodyString := `{"error" : { "code" : "A4C_A2G/257", "message" : { "lang" : "de", "value" : "Already Cloned"} } }` - body := []byte(bodyString) - resp := http.Response{ - Status: "400 Bad Request", - StatusCode: 400, - Body: io.NopCloser(bytes.NewReader(body)), - } - repo := abaputils.Repository{ - Name: "Test", - Branch: "Branch", + Name: "testRepo1", + Branch: "inactie_branch", CommitID: "abcd1234", } - err := errors.New("Custom Error") - err, _ = handleCloneError(&resp, err, autils.ReturnedConnectionDetailsHTTP, client, repo) + apiManager = &abaputils.SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Nanosecond} + err := cloneSingleRepo(apiManager, autils.ReturnedConnectionDetailsHTTP, repo, &config, &autils) assert.NoError(t, err, "Did not expect error") }) - t.Run("Already Cloned, Pull fails", func(t *testing.T) { + t.Run("Already cloned, branch is already checked out, pull instead", func(t *testing.T) { var autils = abaputils.AUtilsMock{} defer autils.Cleanup() @@ -444,130 +455,41 @@ func TestALreadyCloned(t *testing.T) { autils.ReturnedConnectionDetailsHTTP.URL = "https://example.com" autils.ReturnedConnectionDetailsHTTP.Host = "example.com" autils.ReturnedConnectionDetailsHTTP.XCsrfToken = "xcsrftoken" - logResultSuccess := `{"d": { "sc_name": "/DMO/SWC", "status": "S", "to_Log_Overview": { "results": [ { "log_index": 1, "log_name": "Main Import", "type_of_found_issues": "Success", "timestamp": "/Date(1644332299000+0000)/", "to_Log_Protocol": { "results": [ { "log_index": 1, "index_no": "1", "log_name": "", "type": "Info", "descr": "Main import", "timestamp": null, "criticality": 0 } ] } } ] } } }` - client := &abaputils.ClientMock{ - BodyList: []string{ - `{"d" : ` + executionLogStringClone + `}`, - logResultSuccess, - `{"d" : { "EntitySets" : [ "LogOverviews" ] } }`, - `{"d" : { "status" : "E" } }`, - `{"d" : { "status" : "R" } }`, - `{"d" : { "status" : "R" } }`, - `{"d" : ` + executionLogStringClone + `}`, - logResultSuccess, - `{"d" : { "EntitySets" : [ "LogOverviews" ] } }`, - `{"d" : { "status" : "S" } }`, - `{"d" : { "status" : "R" } }`, - `{"d" : { "status" : "R" } }`, - }, - Token: "myToken", - StatusCode: 200, - } - - bodyString := `{"error" : { "code" : "A4C_A2G/257", "message" : { "lang" : "de", "value" : "Already Cloned"} } }` - body := []byte(bodyString) - resp := http.Response{ - Status: "400 Bad Request", - StatusCode: 400, - Body: io.NopCloser(bytes.NewReader(body)), - } - repo := abaputils.Repository{ - Name: "Test", - Branch: "Branch", - CommitID: "abcd1234", - } - - err := errors.New("Custom Error") - err, _ = handleCloneError(&resp, err, autils.ReturnedConnectionDetailsHTTP, client, repo) - if assert.Error(t, err, "Expected error") { - assert.Equal(t, "Pull of the repository / software component 'Test', commit 'abcd1234' failed on the ABAP system: Request to ABAP System not successful", err.Error(), "Expected different error message") + config := abapEnvironmentCloneGitRepoOptions{ + CfAPIEndpoint: "https://api.endpoint.com", + CfOrg: "testOrg", + CfSpace: "testSpace", + CfServiceInstance: "testInstance", + CfServiceKeyName: "testServiceKey", + Username: "testUser", + Password: "testPassword", } - }) - - t.Run("Already Cloned, checkout fails", func(t *testing.T) { - var autils = abaputils.AUtilsMock{} - defer autils.Cleanup() - autils.ReturnedConnectionDetailsHTTP.Password = "password" - autils.ReturnedConnectionDetailsHTTP.User = "user" - autils.ReturnedConnectionDetailsHTTP.URL = "https://example.com" - autils.ReturnedConnectionDetailsHTTP.Host = "example.com" - autils.ReturnedConnectionDetailsHTTP.XCsrfToken = "xcsrftoken" logResultSuccess := `{"d": { "sc_name": "/DMO/SWC", "status": "S", "to_Log_Overview": { "results": [ { "log_index": 1, "log_name": "Main Import", "type_of_found_issues": "Success", "timestamp": "/Date(1644332299000+0000)/", "to_Log_Protocol": { "results": [ { "log_index": 1, "index_no": "1", "log_name": "", "type": "Info", "descr": "Main import", "timestamp": null, "criticality": 0 } ] } } ] } } }` client := &abaputils.ClientMock{ BodyList: []string{ + `{"d" : ` + executionLogStringClone + `}`, logResultSuccess, - `{"d" : { "EntitySets" : [ "LogOverviews" ] } }`, `{"d" : { "status" : "S" } }`, `{"d" : { "status" : "R" } }`, `{"d" : { "status" : "R" } }`, - logResultSuccess, - `{"d" : { "EntitySets" : [ "LogOverviews" ] } }`, - `{"d" : { "status" : "E" } }`, - `{"d" : { "status" : "R" } }`, - `{"d" : { "status" : "R" } }`, + `{"d" : { "sc_name" : "testRepo1", "avail_on_inst" : true, "active_branch": "testBranch1" } }`, + `{"d" : [] }`, }, Token: "myToken", StatusCode: 200, } - bodyString := `{"error" : { "code" : "A4C_A2G/257", "message" : { "lang" : "de", "value" : "Already Cloned"} } }` - body := []byte(bodyString) - resp := http.Response{ - Status: "400 Bad Request", - StatusCode: 400, - Body: io.NopCloser(bytes.NewReader(body)), - } - repo := abaputils.Repository{ - Name: "Test", - Branch: "Branch", + Name: "testRepo1", + Branch: "testBranch1", CommitID: "abcd1234", } - err := errors.New("Custom Error") - err, _ = handleCloneError(&resp, err, autils.ReturnedConnectionDetailsHTTP, client, repo) - if assert.Error(t, err, "Expected error") { - assert.Equal(t, "Something failed during the checkout: Checkout failed: Checkout of branch Branch failed on the ABAP System", err.Error(), "Expected different error message") - } + apiManager = &abaputils.SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Nanosecond} + err := cloneSingleRepo(apiManager, autils.ReturnedConnectionDetailsHTTP, repo, &config, &autils) + assert.NoError(t, err, "Did not expect error") }) - t.Run("Already Cloned, checkout fails", func(t *testing.T) { - - var autils = abaputils.AUtilsMock{} - defer autils.Cleanup() - autils.ReturnedConnectionDetailsHTTP.Password = "password" - autils.ReturnedConnectionDetailsHTTP.User = "user" - autils.ReturnedConnectionDetailsHTTP.URL = "https://example.com" - autils.ReturnedConnectionDetailsHTTP.Host = "example.com" - autils.ReturnedConnectionDetailsHTTP.XCsrfToken = "xcsrftoken" - client := &abaputils.ClientMock{ - BodyList: []string{ - `{"d" : { "status" : "R" } }`, - }, - Token: "myToken", - StatusCode: 200, - } - - bodyString := `{"error" : { "code" : "A4C_A2G/258", "message" : { "lang" : "de", "value" : "Some error message"} } }` - body := []byte(bodyString) - resp := http.Response{ - Status: "400 Bad Request", - StatusCode: 400, - Body: io.NopCloser(bytes.NewReader(body)), - } - - repo := abaputils.Repository{ - Name: "Test", - Branch: "Branch", - CommitID: "abcd1234", - } - - err := errors.New("Custom Error") - err, _ = handleCloneError(&resp, err, autils.ReturnedConnectionDetailsHTTP, client, repo) - if assert.Error(t, err, "Expected error") { - assert.Equal(t, "Custom Error: A4C_A2G/258 - Some error message", err.Error(), "Expected different error message") - } - }) } diff --git a/cmd/abapEnvironmentCreateTag.go b/cmd/abapEnvironmentCreateTag.go index 1681c6c39e..961b062f42 100644 --- a/cmd/abapEnvironmentCreateTag.go +++ b/cmd/abapEnvironmentCreateTag.go @@ -1,10 +1,7 @@ package cmd import ( - "encoding/json" "fmt" - "io" - "net/http/cookiejar" "strings" "time" @@ -16,7 +13,7 @@ import ( "github.com/pkg/errors" ) -func abapEnvironmentCreateTag(config abapEnvironmentCreateTagOptions, telemetryData *telemetry.CustomData) { +func abapEnvironmentCreateTag(config abapEnvironmentCreateTagOptions, _ *telemetry.CustomData) { c := command.Command{} @@ -27,58 +24,36 @@ func abapEnvironmentCreateTag(config abapEnvironmentCreateTagOptions, telemetryD Exec: &c, } - client := piperhttp.Client{} + apiManager := abaputils.SoftwareComponentApiManager{ + Client: &piperhttp.Client{}, + PollIntervall: 5 * time.Second, + } - if err := runAbapEnvironmentCreateTag(&config, telemetryData, &autils, &client); err != nil { + if err := runAbapEnvironmentCreateTag(&config, &autils, &apiManager); err != nil { log.Entry().WithError(err).Fatal("step execution failed") } } -func runAbapEnvironmentCreateTag(config *abapEnvironmentCreateTagOptions, telemetryData *telemetry.CustomData, com abaputils.Communication, client piperhttp.Sender) error { +func runAbapEnvironmentCreateTag(config *abapEnvironmentCreateTagOptions, com abaputils.Communication, apiManager abaputils.SoftwareComponentApiManagerInterface) error { connectionDetails, errorGetInfo := com.GetAbapCommunicationArrangementInfo(convertTagConfig(config), "") if errorGetInfo != nil { return errors.Wrap(errorGetInfo, "Parameters for the ABAP Connection not available") } - // Configuring the HTTP Client and CookieJar - cookieJar, errorCookieJar := cookiejar.New(nil) - if errorCookieJar != nil { - return errors.Wrap(errorCookieJar, "Could not create a Cookie Jar") - } - - client.SetOptions(piperhttp.ClientOptions{ - MaxRequestDuration: 180 * time.Second, - CookieJar: cookieJar, - Username: connectionDetails.User, - Password: connectionDetails.Password, - }) - backlog, errorPrepare := prepareBacklog(config) if errorPrepare != nil { return fmt.Errorf("Something failed during the tag creation: %w", errorPrepare) } - return createTags(backlog, telemetryData, connectionDetails, client, com) + return createTags(backlog, connectionDetails, apiManager) } -func createTags(backlog []CreateTagBacklog, telemetryData *telemetry.CustomData, con abaputils.ConnectionDetailsHTTP, client piperhttp.Sender, com abaputils.Communication) (err error) { - - connection := con - connection.XCsrfToken = "fetch" - connection.URL = con.URL + "/sap/opu/odata/sap/MANAGE_GIT_REPOSITORY/Tags" - resp, err := abaputils.GetHTTPResponse("HEAD", connection, nil, client) - if err != nil { - return abaputils.HandleHTTPError(resp, err, "Authentication on the ABAP system failed", con) - } - defer resp.Body.Close() - - log.Entry().WithField("StatusCode", resp.Status).WithField("ABAP Endpoint", connection.URL).Debug("Authentication on the ABAP system successful") - connection.XCsrfToken = resp.Header.Get("X-Csrf-Token") +func createTags(backlog []abaputils.CreateTagBacklog, con abaputils.ConnectionDetailsHTTP, apiManager abaputils.SoftwareComponentApiManagerInterface) (err error) { errorOccurred := false for _, item := range backlog { - err = createTagsForSingleItem(item, telemetryData, connection, client, com) + err = createTagsForSingleItem(item, con, apiManager) if err != nil { errorOccurred = true } @@ -93,11 +68,11 @@ func createTags(backlog []CreateTagBacklog, telemetryData *telemetry.CustomData, } -func createTagsForSingleItem(item CreateTagBacklog, telemetryData *telemetry.CustomData, con abaputils.ConnectionDetailsHTTP, client piperhttp.Sender, com abaputils.Communication) (err error) { +func createTagsForSingleItem(item abaputils.CreateTagBacklog, con abaputils.ConnectionDetailsHTTP, apiManager abaputils.SoftwareComponentApiManagerInterface) (err error) { errorOccurred := false - for index := range item.tags { - err = createSingleTag(item, index, telemetryData, con, client, com) + for index := range item.Tags { + err = createSingleTag(item, index, con, apiManager) if err != nil { errorOccurred = true } @@ -109,79 +84,38 @@ func createTagsForSingleItem(item CreateTagBacklog, telemetryData *telemetry.Cus return err } -func createSingleTag(item CreateTagBacklog, index int, telemetryData *telemetry.CustomData, con abaputils.ConnectionDetailsHTTP, client piperhttp.Sender, com abaputils.Communication) (err error) { +func createSingleTag(item abaputils.CreateTagBacklog, index int, con abaputils.ConnectionDetailsHTTP, apiManager abaputils.SoftwareComponentApiManagerInterface) (err error) { - requestBodyStruct := CreateTagBody{RepositoryName: item.repositoryName, CommitID: item.commitID, Tag: item.tags[index].tagName, Description: item.tags[index].tagDescription} - requestBodyJson, err := json.Marshal(&requestBodyStruct) - if err != nil { - return err + api, errGetAPI := apiManager.GetAPI(con, abaputils.Repository{Name: item.RepositoryName, CommitID: item.CommitID}) + if errGetAPI != nil { + return errors.Wrap(errGetAPI, "Could not initialize the connection to the system") } - log.Entry().Debugf("Request body: %s", requestBodyJson) - resp, err := abaputils.GetHTTPResponse("POST", con, requestBodyJson, client) - if err != nil { - errorMessage := "Could not create tag " + requestBodyStruct.Tag + " for repository " + requestBodyStruct.RepositoryName + " with commitID " + requestBodyStruct.CommitID - err = abaputils.HandleHTTPError(resp, err, errorMessage, con) - return err + createTagError := api.CreateTag(item.Tags[index]) + if createTagError != nil { + return errors.Wrapf(err, "Creation of Tag failed on the ABAP system") } - defer resp.Body.Close() - // Parse response - var createTagResponse CreateTagResponse - var abapResp map[string]*json.RawMessage - bodyText, _ := io.ReadAll(resp.Body) + status, errorPollEntity := abaputils.PollEntity(api, apiManager.GetPollIntervall()) - if err = json.Unmarshal(bodyText, &abapResp); err != nil { - return err - } - if err = json.Unmarshal(*abapResp["d"], &createTagResponse); err != nil { - return err - } - - con.URL = con.Host + "/sap/opu/odata/sap/MANAGE_GIT_REPOSITORY/Pull(guid'" + createTagResponse.UUID + "')" - err = checkStatus(con, client, com) - - if err == nil { - log.Entry().Info("Created tag " + requestBodyStruct.Tag + " for repository " + requestBodyStruct.RepositoryName + " with commitID " + requestBodyStruct.CommitID) + if errorPollEntity == nil && status == "S" { + log.Entry().Info("Created tag " + item.Tags[index].TagName + " for repository " + item.RepositoryName + " with commitID " + item.CommitID) } else { - log.Entry().Error("NOT created: Tag " + requestBodyStruct.Tag + " for repository " + requestBodyStruct.RepositoryName + " with commitID " + requestBodyStruct.CommitID) + log.Entry().Error("NOT created: Tag " + item.Tags[index].TagName + " for repository " + item.RepositoryName + " with commitID " + item.CommitID) + err = errors.New("Creation of Tag failed on the ABAP system") } return err } -func checkStatus(con abaputils.ConnectionDetailsHTTP, client piperhttp.Sender, com abaputils.Communication) (err error) { - var status string - pollIntervall := com.GetPollIntervall() - count := 0 - for { - count += 1 - entity, _, err := abaputils.GetStatus("Could not create Tag", con, client) - if err != nil { - return err - } - status = entity.Status - if status != "R" { - if status == "E" { - err = errors.New("Could not create Tag") - } - return err - } - if count >= 200 { - return errors.New("Could not create Tag (Timeout)") - } - time.Sleep(pollIntervall) - } -} - -func prepareBacklog(config *abapEnvironmentCreateTagOptions) (backlog []CreateTagBacklog, err error) { +func prepareBacklog(config *abapEnvironmentCreateTagOptions) (backlog []abaputils.CreateTagBacklog, err error) { if config.Repositories != "" && config.RepositoryName != "" { return nil, errors.New("Configuring the parameter repositories and the parameter repositoryName at the same time is not allowed") } if config.RepositoryName != "" && config.CommitID != "" { - backlog = append(backlog, CreateTagBacklog{repositoryName: config.RepositoryName, commitID: config.CommitID}) + backlog = append(backlog, abaputils.CreateTagBacklog{RepositoryName: config.RepositoryName, CommitID: config.CommitID}) } if config.Repositories != "" { @@ -190,10 +124,10 @@ func prepareBacklog(config *abapEnvironmentCreateTagOptions) (backlog []CreateTa return nil, err } for _, repo := range descriptor.Repositories { - backlogInstance := CreateTagBacklog{repositoryName: repo.Name, commitID: repo.CommitID} + backlogInstance := abaputils.CreateTagBacklog{RepositoryName: repo.Name, CommitID: repo.CommitID} if config.GenerateTagForAddonComponentVersion && repo.VersionYAML != "" { - tag := Tag{tagName: "v" + repo.VersionYAML, tagDescription: "Generated by the ABAP Environment Pipeline"} - backlogInstance.tags = append(backlogInstance.tags, tag) + tag := abaputils.Tag{TagName: "v" + repo.VersionYAML, TagDescription: "Generated by the ABAP Environment Pipeline"} + backlogInstance.Tags = append(backlogInstance.Tags, tag) } backlog = append(backlog, backlogInstance) } @@ -212,11 +146,11 @@ func prepareBacklog(config *abapEnvironmentCreateTagOptions) (backlog []CreateTa return backlog, nil } -func addTagToList(backlog []CreateTagBacklog, tag string, description string) []CreateTagBacklog { +func addTagToList(backlog []abaputils.CreateTagBacklog, tag string, description string) []abaputils.CreateTagBacklog { for i, item := range backlog { - tag := Tag{tagName: tag, tagDescription: description} - backlog[i].tags = append(item.tags, tag) + tag := abaputils.Tag{TagName: tag, TagDescription: description} + backlog[i].Tags = append(item.Tags, tag) } return backlog } @@ -235,25 +169,3 @@ func convertTagConfig(config *abapEnvironmentCreateTagOptions) abaputils.AbapEnv return subOptions } - -type CreateTagBacklog struct { - repositoryName string - commitID string - tags []Tag -} - -type Tag struct { - tagName string - tagDescription string -} - -type CreateTagBody struct { - RepositoryName string `json:"sc_name"` - CommitID string `json:"commit_id"` - Tag string `json:"tag_name"` - Description string `json:"tag_description"` -} - -type CreateTagResponse struct { - UUID string `json:"uuid"` -} diff --git a/cmd/abapEnvironmentCreateTag_test.go b/cmd/abapEnvironmentCreateTag_test.go index 1241d3639c..996cafd2bf 100644 --- a/cmd/abapEnvironmentCreateTag_test.go +++ b/cmd/abapEnvironmentCreateTag_test.go @@ -4,8 +4,10 @@ package cmd import ( + "encoding/json" "os" "testing" + "time" "github.com/SAP/jenkins-library/pkg/abaputils" "github.com/SAP/jenkins-library/pkg/log" @@ -13,6 +15,28 @@ import ( "github.com/stretchr/testify/assert" ) +var executionLogStringCreateTag string +var logResultSuccess string + +func init() { + logResultSuccess = `{"d": { "sc_name": "/DMO/SWC", "status": "S", "to_Log_Overview": { "results": [ { "log_index": 1, "log_name": "Main Import", "type_of_found_issues": "Success", "timestamp": "/Date(1644332299000+0000)/", "to_Log_Protocol": { "results": [ { "log_index": 1, "index_no": "1", "log_name": "", "type": "Info", "descr": "Main import", "timestamp": null, "criticality": 0 } ] } } ] } } }` + executionLog := abaputils.LogProtocolResults{ + Results: []abaputils.LogProtocol{ + { + ProtocolLine: 1, + OverviewIndex: 1, + Type: "LogEntry", + Description: "S", + Timestamp: "/Date(1644332299000+0000)/", + }, + }, + Count: "1", + } + executionLogResponse, _ := json.Marshal(executionLog) + executionLogStringCreateTag = string(executionLogResponse) + +} + func TestRunAbapEnvironmentCreateTag(t *testing.T) { t.Run("happy path", func(t *testing.T) { @@ -56,10 +80,16 @@ repositories: } client := &abaputils.ClientMock{ BodyList: []string{ + `{"d" : ` + executionLogStringClone + `}`, + logResultSuccess, `{"d" : { "Status" : "S" } }`, `{"d" : { "uuid" : "abc" } }`, + `{"d" : ` + executionLogStringClone + `}`, + logResultSuccess, `{"d" : { "Status" : "S" } }`, `{"d" : { "uuid" : "abc" } }`, + `{"d" : ` + executionLogStringClone + `}`, + logResultSuccess, `{"d" : { "Status" : "S" } }`, `{"d" : { "uuid" : "abc" } }`, `{"d" : { "empty" : "body" } }`, @@ -71,13 +101,14 @@ repositories: _, hook := test.NewNullLogger() log.RegisterHook(hook) - err = runAbapEnvironmentCreateTag(config, nil, autils, client) + apiManager = &abaputils.SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + err = runAbapEnvironmentCreateTag(config, autils, apiManager) assert.NoError(t, err, "Did not expect error") - assert.Equal(t, 3, len(hook.Entries), "Expected a different number of entries") - assert.Equal(t, `Created tag v4.5.6 for repository /DMO/SWC with commitID 1234abcd`, hook.AllEntries()[0].Message, "Expected a different message") - assert.Equal(t, `Created tag -DMO-PRODUCT-1.2.3 for repository /DMO/SWC with commitID 1234abcd`, hook.AllEntries()[1].Message, "Expected a different message") - assert.Equal(t, `Created tag tag for repository /DMO/SWC with commitID 1234abcd`, hook.AllEntries()[2].Message, "Expected a different message") + assert.Equal(t, 22, len(hook.Entries), "Expected a different number of entries") + assert.Equal(t, `Created tag v4.5.6 for repository /DMO/SWC with commitID 1234abcd`, hook.AllEntries()[11].Message, "Expected a different message") + assert.Equal(t, `Created tag -DMO-PRODUCT-1.2.3 for repository /DMO/SWC with commitID 1234abcd`, hook.AllEntries()[16].Message, "Expected a different message") + assert.Equal(t, `Created tag tag for repository /DMO/SWC with commitID 1234abcd`, hook.AllEntries()[21].Message, "Expected a different message") hook.Reset() }) @@ -122,10 +153,18 @@ repositories: } client := &abaputils.ClientMock{ BodyList: []string{ + `{"d" : ` + executionLogStringClone + `}`, + logResultSuccess, `{"d" : { "Status" : "E" } }`, `{"d" : { "uuid" : "abc" } }`, + `{"d" : { "empty" : "body" } }`, + `{"d" : ` + executionLogStringClone + `}`, + logResultSuccess, `{"d" : { "Status" : "E" } }`, `{"d" : { "uuid" : "abc" } }`, + `{"d" : { "empty" : "body" } }`, + `{"d" : ` + executionLogStringClone + `}`, + logResultSuccess, `{"d" : { "Status" : "E" } }`, `{"d" : { "uuid" : "abc" } }`, `{"d" : { "empty" : "body" } }`, @@ -137,14 +176,15 @@ repositories: _, hook := test.NewNullLogger() log.RegisterHook(hook) - err = runAbapEnvironmentCreateTag(config, nil, autils, client) + apiManager = &abaputils.SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + err = runAbapEnvironmentCreateTag(config, autils, apiManager) assert.Error(t, err, "Did expect error") - assert.Equal(t, 4, len(hook.Entries), "Expected a different number of entries") - assert.Equal(t, `NOT created: Tag v4.5.6 for repository /DMO/SWC with commitID 1234abcd`, hook.AllEntries()[0].Message, "Expected a different message") - assert.Equal(t, `NOT created: Tag -DMO-PRODUCT-1.2.3 for repository /DMO/SWC with commitID 1234abcd`, hook.AllEntries()[1].Message, "Expected a different message") - assert.Equal(t, `NOT created: Tag tag for repository /DMO/SWC with commitID 1234abcd`, hook.AllEntries()[2].Message, "Expected a different message") - assert.Equal(t, `At least one tag has not been created`, hook.AllEntries()[3].Message, "Expected a different message") + assert.Equal(t, 37, len(hook.Entries), "Expected a different number of entries") + assert.Equal(t, `NOT created: Tag v4.5.6 for repository /DMO/SWC with commitID 1234abcd`, hook.AllEntries()[11].Message, "Expected a different message") + assert.Equal(t, `NOT created: Tag -DMO-PRODUCT-1.2.3 for repository /DMO/SWC with commitID 1234abcd`, hook.AllEntries()[23].Message, "Expected a different message") + assert.Equal(t, `NOT created: Tag tag for repository /DMO/SWC with commitID 1234abcd`, hook.AllEntries()[35].Message, "Expected a different message") + assert.Equal(t, `At least one tag has not been created`, hook.AllEntries()[36].Message, "Expected a different message") hook.Reset() }) @@ -175,6 +215,8 @@ func TestRunAbapEnvironmentCreateTagConfigurations(t *testing.T) { } client := &abaputils.ClientMock{ BodyList: []string{ + `{"d" : ` + executionLogStringClone + `}`, + logResultSuccess, `{"d" : { "Status" : "S" } }`, `{"d" : { "uuid" : "abc" } }`, `{"d" : { "empty" : "body" } }`, @@ -186,11 +228,12 @@ func TestRunAbapEnvironmentCreateTagConfigurations(t *testing.T) { _, hook := test.NewNullLogger() log.RegisterHook(hook) - err := runAbapEnvironmentCreateTag(config, nil, autils, client) + apiManager = &abaputils.SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + err := runAbapEnvironmentCreateTag(config, autils, apiManager) assert.NoError(t, err, "Did not expect error") - assert.Equal(t, 1, len(hook.Entries), "Expected a different number of entries") - assert.Equal(t, `Created tag tag for repository /DMO/SWC with commitID 1234abcd`, hook.AllEntries()[0].Message, "Expected a different message") + assert.Equal(t, 12, len(hook.Entries), "Expected a different number of entries") + assert.Equal(t, `Created tag tag for repository /DMO/SWC with commitID 1234abcd`, hook.AllEntries()[11].Message, "Expected a different message") hook.Reset() }) @@ -253,7 +296,8 @@ repositories: StatusCode: 200, } - err = runAbapEnvironmentCreateTag(config, nil, autils, client) + apiManager = &abaputils.SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + err = runAbapEnvironmentCreateTag(config, autils, apiManager) assert.Error(t, err, "Did expect error") assert.Equal(t, "Something failed during the tag creation: Configuring the parameter repositories and the parameter repositoryName at the same time is not allowed", err.Error(), "Expected different error message") @@ -315,11 +359,12 @@ repositories: _, hook := test.NewNullLogger() log.RegisterHook(hook) - err = runAbapEnvironmentCreateTag(config, nil, autils, client) + apiManager = &abaputils.SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + err = runAbapEnvironmentCreateTag(config, autils, apiManager) assert.NoError(t, err, "Did not expect error") - assert.Equal(t, 1, len(hook.Entries), "Expected a different number of entries") - assert.Equal(t, `Created tag tag for repository /DMO/SWC with commitID 1234abcd`, hook.AllEntries()[0].Message, "Expected a different message") + assert.Equal(t, 5, len(hook.Entries), "Expected a different number of entries") + assert.Equal(t, `Created tag tag for repository /DMO/SWC with commitID 1234abcd`, hook.AllEntries()[4].Message, "Expected a different message") hook.Reset() }) diff --git a/cmd/abapEnvironmentPullGitRepo.go b/cmd/abapEnvironmentPullGitRepo.go index 5dae599e1a..8c93a2d55c 100644 --- a/cmd/abapEnvironmentPullGitRepo.go +++ b/cmd/abapEnvironmentPullGitRepo.go @@ -1,11 +1,7 @@ package cmd import ( - "encoding/json" "fmt" - "io" - "net/http/cookiejar" - "reflect" "time" "github.com/SAP/jenkins-library/pkg/abaputils" @@ -28,38 +24,28 @@ func abapEnvironmentPullGitRepo(options abapEnvironmentPullGitRepoOptions, _ *te Exec: &c, } - client := piperhttp.Client{} + apiManager := abaputils.SoftwareComponentApiManager{ + Client: &piperhttp.Client{}, + PollIntervall: 5 * time.Second, + } // error situations should stop execution through log.Entry().Fatal() call which leads to an os.Exit(1) in the end - err := runAbapEnvironmentPullGitRepo(&options, &autils, &client) + err := runAbapEnvironmentPullGitRepo(&options, &autils, &apiManager) if err != nil { log.Entry().WithError(err).Fatal("step execution failed") } } -func runAbapEnvironmentPullGitRepo(options *abapEnvironmentPullGitRepoOptions, com abaputils.Communication, client piperhttp.Sender) (err error) { +func runAbapEnvironmentPullGitRepo(options *abapEnvironmentPullGitRepoOptions, com abaputils.Communication, apiManager abaputils.SoftwareComponentApiManagerInterface) (err error) { subOptions := convertPullConfig(options) // Determine the host, user and password, either via the input parameters or via a cloud foundry service key - connectionDetails, err := com.GetAbapCommunicationArrangementInfo(subOptions, "/sap/opu/odata/sap/MANAGE_GIT_REPOSITORY/Pull") + connectionDetails, err := com.GetAbapCommunicationArrangementInfo(subOptions, "") if err != nil { return errors.Wrap(err, "Parameters for the ABAP Connection not available") } - cookieJar, err := cookiejar.New(nil) - if err != nil { - return errors.Wrap(err, "Could not create a Cookie Jar") - } - clientOptions := piperhttp.ClientOptions{ - MaxRequestDuration: 180 * time.Second, - CookieJar: cookieJar, - Username: connectionDetails.User, - Password: connectionDetails.Password, - } - client.SetOptions(clientOptions) - pollIntervall := com.GetPollIntervall() - var repositories []abaputils.Repository err = checkPullRepositoryConfiguration(*options) if err != nil { @@ -71,15 +57,15 @@ func runAbapEnvironmentPullGitRepo(options *abapEnvironmentPullGitRepoOptions, c return err } - err = pullRepositories(repositories, connectionDetails, client, pollIntervall) + err = pullRepositories(repositories, connectionDetails, apiManager) return err } -func pullRepositories(repositories []abaputils.Repository, pullConnectionDetails abaputils.ConnectionDetailsHTTP, client piperhttp.Sender, pollIntervall time.Duration) (err error) { +func pullRepositories(repositories []abaputils.Repository, pullConnectionDetails abaputils.ConnectionDetailsHTTP, apiManager abaputils.SoftwareComponentApiManagerInterface) (err error) { log.Entry().Infof("Start pulling %v repositories", len(repositories)) for _, repo := range repositories { - err = handlePull(repo, pullConnectionDetails, client, pollIntervall) + err = handlePull(repo, pullConnectionDetails, apiManager) if err != nil { break } @@ -90,22 +76,27 @@ func pullRepositories(repositories []abaputils.Repository, pullConnectionDetails return err } -func handlePull(repo abaputils.Repository, pullConnectionDetails abaputils.ConnectionDetailsHTTP, client piperhttp.Sender, pollIntervall time.Duration) (err error) { +func handlePull(repo abaputils.Repository, con abaputils.ConnectionDetailsHTTP, apiManager abaputils.SoftwareComponentApiManagerInterface) (err error) { logString := repo.GetPullLogString() errorString := "Pull of the " + logString + " failed on the ABAP system" - abaputils.AddDefaultDashedLine() + abaputils.AddDefaultDashedLine(1) log.Entry().Info("Start pulling the " + logString) - abaputils.AddDefaultDashedLine() + abaputils.AddDefaultDashedLine(1) + + api, errGetAPI := apiManager.GetAPI(con, repo) + if errGetAPI != nil { + return errors.Wrap(errGetAPI, "Could not initialize the connection to the system") + } - uriConnectionDetails, err := triggerPull(repo, pullConnectionDetails, client) + err = api.Pull() if err != nil { return errors.Wrapf(err, errorString) } // Polling the status of the repository import on the ABAP Environment system - status, errorPollEntity := abaputils.PollEntity(repo.Name, uriConnectionDetails, client, pollIntervall) + status, errorPollEntity := abaputils.PollEntity(api, apiManager.GetPollIntervall()) if errorPollEntity != nil { return errors.Wrapf(errorPollEntity, errorString) } @@ -116,61 +107,6 @@ func handlePull(repo abaputils.Repository, pullConnectionDetails abaputils.Conne return err } -func triggerPull(repo abaputils.Repository, pullConnectionDetails abaputils.ConnectionDetailsHTTP, client piperhttp.Sender) (abaputils.ConnectionDetailsHTTP, error) { - - uriConnectionDetails := pullConnectionDetails - uriConnectionDetails.URL = "" - pullConnectionDetails.XCsrfToken = "fetch" - - // Loging into the ABAP System - getting the x-csrf-token and cookies - resp, err := abaputils.GetHTTPResponse("HEAD", pullConnectionDetails, nil, client) - if err != nil { - err = abaputils.HandleHTTPError(resp, err, "Authentication on the ABAP system failed", pullConnectionDetails) - return uriConnectionDetails, err - } - defer resp.Body.Close() - - log.Entry().WithField("StatusCode", resp.Status).WithField("ABAP Endpoint", pullConnectionDetails.URL).Debug("Authentication on the ABAP system successful") - uriConnectionDetails.XCsrfToken = resp.Header.Get("X-Csrf-Token") - pullConnectionDetails.XCsrfToken = uriConnectionDetails.XCsrfToken - - // Trigger the Pull of a Repository - if repo.Name == "" { - return uriConnectionDetails, errors.New("An empty string was passed for the parameter 'repositoryName'") - } - - jsonBody := []byte(repo.GetPullRequestBody()) - resp, err = abaputils.GetHTTPResponse("POST", pullConnectionDetails, jsonBody, client) - if err != nil { - err = abaputils.HandleHTTPError(resp, err, "Could not pull the "+repo.GetPullLogString(), uriConnectionDetails) - return uriConnectionDetails, err - } - defer resp.Body.Close() - log.Entry().WithField("StatusCode", resp.Status).WithField("repositoryName", repo.Name).WithField("commitID", repo.CommitID).WithField("Tag", repo.Tag).Debug("Triggered Pull of repository / software component") - - // Parse Response - var body abaputils.PullEntity - var abapResp map[string]*json.RawMessage - bodyText, errRead := io.ReadAll(resp.Body) - if errRead != nil { - return uriConnectionDetails, err - } - if err := json.Unmarshal(bodyText, &abapResp); err != nil { - return uriConnectionDetails, err - } - if err := json.Unmarshal(*abapResp["d"], &body); err != nil { - return uriConnectionDetails, err - } - if reflect.DeepEqual(abaputils.PullEntity{}, body) { - log.Entry().WithField("StatusCode", resp.Status).WithField("repositoryName", repo.Name).WithField("commitID", repo.CommitID).WithField("Tag", repo.Tag).Error("Could not pull the repository / software component") - err := errors.New("Request to ABAP System not successful") - return uriConnectionDetails, err - } - - uriConnectionDetails.URL = body.Metadata.URI - return uriConnectionDetails, nil -} - func checkPullRepositoryConfiguration(options abapEnvironmentPullGitRepoOptions) error { if (len(options.RepositoryNames) > 0 && options.Repositories != "") || (len(options.RepositoryNames) > 0 && options.RepositoryName != "") || (options.RepositoryName != "" && options.Repositories != "") { @@ -183,7 +119,7 @@ func checkPullRepositoryConfiguration(options abapEnvironmentPullGitRepoOptions) } func finishPullLogs() { - abaputils.AddDefaultDashedLine() + abaputils.AddDefaultDashedLine(1) log.Entry().Info("All repositories were pulled successfully") } diff --git a/cmd/abapEnvironmentPullGitRepo_test.go b/cmd/abapEnvironmentPullGitRepo_test.go index a941b7b91f..50e0c5e223 100644 --- a/cmd/abapEnvironmentPullGitRepo_test.go +++ b/cmd/abapEnvironmentPullGitRepo_test.go @@ -7,9 +7,9 @@ import ( "encoding/json" "os" "testing" + "time" "github.com/SAP/jenkins-library/pkg/abaputils" - "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) @@ -58,7 +58,6 @@ func TestPullStep(t *testing.T) { logResultSuccess := `{"d": { "sc_name": "/DMO/SWC", "status": "S", "to_Log_Overview": { "results": [ { "log_index": 1, "log_name": "Main Import", "type_of_found_issues": "Success", "timestamp": "/Date(1644332299000+0000)/", "to_Log_Protocol": { "results": [ { "log_index": 1, "index_no": "1", "log_name": "", "type": "Info", "descr": "Main import", "timestamp": null, "criticality": 0 } ] } } ] } } }` client := &abaputils.ClientMock{ BodyList: []string{ - `{"d" : [] }`, `{"d" : ` + executionLogStringPull + `}`, logResultSuccess, `{"d" : { "status" : "S" } }`, @@ -70,7 +69,8 @@ func TestPullStep(t *testing.T) { StatusCode: 200, } - err := runAbapEnvironmentPullGitRepo(&config, &autils, client) + apiManager = &abaputils.SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + err := runAbapEnvironmentPullGitRepo(&config, &autils, apiManager) assert.NoError(t, err, "Did not expect error") assert.Equal(t, 0, len(client.BodyList), "Not all requests were done") }) @@ -95,7 +95,9 @@ func TestPullStep(t *testing.T) { } config := abapEnvironmentPullGitRepoOptions{} - err := runAbapEnvironmentPullGitRepo(&config, &autils, client) + + apiManager = &abaputils.SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + err := runAbapEnvironmentPullGitRepo(&config, &autils, apiManager) assert.Equal(t, expectedErrorMessage, err.Error(), "Different error message expected") }) @@ -145,7 +147,8 @@ repositories: Password: "testPassword", Repositories: "repositoriesTest.yml", } - err = runAbapEnvironmentPullGitRepo(&config, &autils, client) + apiManager = &abaputils.SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + err = runAbapEnvironmentPullGitRepo(&config, &autils, apiManager) assert.NoError(t, err) }) @@ -200,7 +203,8 @@ repositories: StatusCode: 200, } - err = runAbapEnvironmentPullGitRepo(&config, &autils, client) + apiManager = &abaputils.SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + err = runAbapEnvironmentPullGitRepo(&config, &autils, apiManager) if assert.Error(t, err, "Expected error") { assert.Equal(t, "Pull of the repository / software component '/DMO/REPO_A', commit 'ABCD1234' failed on the ABAP system", err.Error(), "Expected different error message") } @@ -258,7 +262,8 @@ repositories: StatusCode: 200, } - err = runAbapEnvironmentPullGitRepo(&config, &autils, client) + apiManager = &abaputils.SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + err = runAbapEnvironmentPullGitRepo(&config, &autils, apiManager) if assert.Error(t, err, "Expected error") { assert.Equal(t, "Pull of the repository / software component '/DMO/REPO_A', tag 'v-1.0.1-build-0001' failed on the ABAP system", err.Error(), "Expected different error message") } @@ -297,7 +302,8 @@ repositories: StatusCode: 200, } - err := runAbapEnvironmentPullGitRepo(&config, &autils, client) + apiManager = &abaputils.SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + err := runAbapEnvironmentPullGitRepo(&config, &autils, apiManager) if assert.Error(t, err, "Expected error") { assert.Equal(t, "Pull of the repository / software component '/DMO/SWC', commit '123456' failed on the ABAP system", err.Error(), "Expected different error message") } @@ -335,7 +341,8 @@ repositories: StatusCode: 200, } - err := runAbapEnvironmentPullGitRepo(&config, &autils, client) + apiManager = &abaputils.SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + err := runAbapEnvironmentPullGitRepo(&config, &autils, apiManager) if assert.Error(t, err, "Expected error") { assert.Equal(t, "Pull of the repository / software component '/DMO/SWC' failed on the ABAP system", err.Error(), "Expected different error message") } @@ -381,7 +388,8 @@ repositories: Password: "testPassword", Repositories: "repositoriesTest.yml", } - err = runAbapEnvironmentPullGitRepo(&config, &autils, client) + apiManager = &abaputils.SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + err = runAbapEnvironmentPullGitRepo(&config, &autils, apiManager) assert.EqualError(t, err, expectedErrorMessage) }) @@ -430,66 +438,12 @@ repositories: Password: "testPassword", Repositories: "repositoriesTest.yml", } - err = runAbapEnvironmentPullGitRepo(&config, &autils, client) + apiManager = &abaputils.SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + err = runAbapEnvironmentPullGitRepo(&config, &autils, apiManager) assert.EqualError(t, err, expectedErrorMessage) }) } -func TestTriggerPull(t *testing.T) { - - t.Run("Test trigger pull: success case", func(t *testing.T) { - - receivedURI := "example.com/Entity" - uriExpected := receivedURI - tokenExpected := "myToken" - - client := &abaputils.ClientMock{ - Body: `{"d" : { "__metadata" : { "uri" : "` + receivedURI + `" } } }`, - Token: tokenExpected, - StatusCode: 200, - } - - repoName := "testRepo1" - testCommit := "9caede7f31028cd52333eb496434275687fefb47" - - con := abaputils.ConnectionDetailsHTTP{ - User: "MY_USER", - Password: "MY_PW", - URL: "https://api.endpoint.com/Entity/", - } - entityConnection, err := triggerPull(abaputils.Repository{Name: repoName, CommitID: testCommit}, con, client) - assert.Nil(t, err) - assert.Equal(t, uriExpected, entityConnection.URL) - assert.Equal(t, tokenExpected, entityConnection.XCsrfToken) - }) - - t.Run("Test trigger pull: ABAP Error", func(t *testing.T) { - - errorMessage := "ABAP Error Message" - errorCode := "ERROR/001" - HTTPErrorMessage := "HTTP Error Message" - combinedErrorMessage := "HTTP Error Message: ERROR/001 - ABAP Error Message" - - client := &abaputils.ClientMock{ - Body: `{"error" : { "code" : "` + errorCode + `", "message" : { "lang" : "en", "value" : "` + errorMessage + `" } } }`, - Token: "myToken", - StatusCode: 400, - Error: errors.New(HTTPErrorMessage), - } - - repoName := "testRepo1" - testCommit := "9caede7f31028cd52333eb496434275687fefb47" - - con := abaputils.ConnectionDetailsHTTP{ - User: "MY_USER", - Password: "MY_PW", - URL: "https://api.endpoint.com/Entity/", - } - _, err := triggerPull(abaputils.Repository{Name: repoName, CommitID: testCommit}, con, client) - assert.Equal(t, combinedErrorMessage, err.Error(), "Different error message expected") - }) -} - func TestPullConfigChecker(t *testing.T) { t.Run("Success case: check config file", func(t *testing.T) { config := abapEnvironmentPullGitRepoOptions{ diff --git a/cmd/abapEnvironmentPushATCSystemConfig.go b/cmd/abapEnvironmentPushATCSystemConfig.go index 22f1cf00a3..2ddcd51ea3 100644 --- a/cmd/abapEnvironmentPushATCSystemConfig.go +++ b/cmd/abapEnvironmentPushATCSystemConfig.go @@ -199,7 +199,7 @@ func fetchXcsrfTokenFromHead(connectionDetails abaputils.ConnectionDetailsHTTP, // Loging into the ABAP System - getting the x-csrf-token and cookies resp, err := abaputils.GetHTTPResponse("HEAD", connectionDetails, nil, client) if err != nil { - err = abaputils.HandleHTTPError(resp, err, "authentication on the ABAP system failed", connectionDetails) + _, err = abaputils.HandleHTTPError(resp, err, "authentication on the ABAP system failed", connectionDetails) return connectionDetails.XCsrfToken, errors.Errorf("X-Csrf-Token fetch failed for Service ATC System Configuration: %v", err) } defer resp.Body.Close() diff --git a/cmd/abapEnvironmentRunATCCheck.go b/cmd/abapEnvironmentRunATCCheck.go index 1487497b63..3fdabde3ff 100644 --- a/cmd/abapEnvironmentRunATCCheck.go +++ b/cmd/abapEnvironmentRunATCCheck.go @@ -306,7 +306,7 @@ func runATC(requestType string, details abaputils.ConnectionDetailsHTTP, body [] resp, err := client.SendRequest(requestType, details.URL, bytes.NewBuffer(body), header, nil) _ = logResponseBody(resp) if err != nil || (resp != nil && resp.StatusCode == 400) { // send request does not seem to produce error with StatusCode 400!!! - err = abaputils.HandleHTTPError(resp, err, "triggering ATC run failed with Status: "+resp.Status, details) + _, err = abaputils.HandleHTTPError(resp, err, "triggering ATC run failed with Status: "+resp.Status, details) log.SetErrorCategory(log.ErrorService) return resp, errors.Errorf("triggering ATC run failed: %v", err) } diff --git a/pkg/abaputils/abaputils.go b/pkg/abaputils/abaputils.go index 738b6715f3..9b944cdc31 100644 --- a/pkg/abaputils/abaputils.go +++ b/pkg/abaputils/abaputils.go @@ -167,6 +167,9 @@ func ReadConfigFile(path string) (file []byte, err error) { // GetHTTPResponse wraps the SendRequest function of piperhttp func GetHTTPResponse(requestType string, connectionDetails ConnectionDetailsHTTP, body []byte, client piperhttp.Sender) (*http.Response, error) { + log.Entry().Debugf("Request body: %s", string(body)) + log.Entry().Debugf("Request user: %s", connectionDetails.User) + header := make(map[string][]string) header["Content-Type"] = []string{"application/json"} header["Accept"] = []string{"application/json"} @@ -182,16 +185,20 @@ func GetHTTPResponse(requestType string, connectionDetails ConnectionDetailsHTTP // Further error details may be present in the response body of the HTTP response. // If the response body is parseable, the included details are wrapped around the original error from the HTTP repsponse. // If this is not possible, the original error is returned. -func HandleHTTPError(resp *http.Response, err error, message string, connectionDetails ConnectionDetailsHTTP) error { +func HandleHTTPError(resp *http.Response, err error, message string, connectionDetails ConnectionDetailsHTTP) (string, error) { + + var errorText string + var errorCode string + var parsingError error if resp == nil { // Response is nil in case of a timeout log.Entry().WithError(err).WithField("ABAP Endpoint", connectionDetails.URL).Error("Request failed") match, _ := regexp.MatchString(".*EOF$", err.Error()) if match { - AddDefaultDashedLine() + AddDefaultDashedLine(1) log.Entry().Infof("%s", "A connection could not be established to the ABAP system. The typical root cause is the network configuration (firewall, IP allowlist, etc.)") - AddDefaultDashedLine() + AddDefaultDashedLine(1) } log.Entry().Infof("Error message: %s,", err.Error()) @@ -201,15 +208,15 @@ func HandleHTTPError(resp *http.Response, err error, message string, connectionD log.Entry().WithField("StatusCode", resp.Status).WithField("User", connectionDetails.User).WithField("URL", connectionDetails.URL).Error(message) - errorText, errorCode, parsingError := GetErrorDetailsFromResponse(resp) + errorText, errorCode, parsingError = GetErrorDetailsFromResponse(resp) if parsingError != nil { - return err + return "", err } abapError := errors.New(fmt.Sprintf("%s - %s", errorCode, errorText)) err = errors.Wrap(abapError, err.Error()) } - return err + return errorCode, err } func GetErrorDetailsFromResponse(resp *http.Response) (errorString string, errorCode string, err error) { @@ -249,8 +256,10 @@ func ConvertTime(logTimeStamp string) time.Time { } // AddDefaultDashedLine adds 25 dashes -func AddDefaultDashedLine() { - log.Entry().Infof(strings.Repeat("-", 25)) +func AddDefaultDashedLine(j int) { + for i := 1; i <= j; i++ { + log.Entry().Infof(strings.Repeat("-", 25)) + } } // AddDefaultDebugLine adds 25 dashes in debug @@ -370,6 +379,7 @@ type ClientMock struct { Error error NilResponse bool ErrorInsteadOfDump bool + ErrorList []error } // SetOptions sets clientOptions for a client mock @@ -383,8 +393,10 @@ func (c *ClientMock) SendRequest(method, url string, bdy io.Reader, hdr http.Hea } var body []byte + var responseError error if c.Body != "" { body = []byte(c.Body) + responseError = c.Error } else { if c.ErrorInsteadOfDump && len(c.BodyList) == 0 { return nil, errors.New("No more bodies in the list") @@ -392,6 +404,12 @@ func (c *ClientMock) SendRequest(method, url string, bdy io.Reader, hdr http.Hea bodyString := c.BodyList[len(c.BodyList)-1] c.BodyList = c.BodyList[:len(c.BodyList)-1] body = []byte(bodyString) + if len(c.ErrorList) == 0 { + responseError = c.Error + } else { + responseError = c.ErrorList[len(c.ErrorList)-1] + c.ErrorList = c.ErrorList[:len(c.ErrorList)-1] + } } header := http.Header{} header.Set("X-Csrf-Token", c.Token) @@ -399,7 +417,7 @@ func (c *ClientMock) SendRequest(method, url string, bdy io.Reader, hdr http.Hea StatusCode: c.StatusCode, Header: header, Body: io.NopCloser(bytes.NewReader(body)), - }, c.Error + }, responseError } // DownloadFile : Empty file download diff --git a/pkg/abaputils/abaputils_test.go b/pkg/abaputils/abaputils_test.go index f81dd63dd7..8bfc272f93 100644 --- a/pkg/abaputils/abaputils_test.go +++ b/pkg/abaputils/abaputils_test.go @@ -309,7 +309,7 @@ func TestHandleHTTPError(t *testing.T) { receivedErr := errors.New(errorValue) message := "Custom Error Message" - err := HandleHTTPError(&resp, receivedErr, message, ConnectionDetailsHTTP{}) + _, err := HandleHTTPError(&resp, receivedErr, message, ConnectionDetailsHTTP{}) assert.EqualError(t, err, fmt.Sprintf("%s: %s - %s", receivedErr.Error(), abapErrorCode, abapErrorMessage)) log.Entry().Info(err.Error()) }) @@ -328,7 +328,7 @@ func TestHandleHTTPError(t *testing.T) { receivedErr := errors.New(errorValue) message := "Custom Error Message" - err := HandleHTTPError(&resp, receivedErr, message, ConnectionDetailsHTTP{}) + _, err := HandleHTTPError(&resp, receivedErr, message, ConnectionDetailsHTTP{}) assert.EqualError(t, err, fmt.Sprintf("%s", receivedErr.Error())) log.Entry().Info(err.Error()) }) @@ -347,7 +347,7 @@ func TestHandleHTTPError(t *testing.T) { receivedErr := errors.New(errorValue) message := "Custom Error Message" - err := HandleHTTPError(&resp, receivedErr, message, ConnectionDetailsHTTP{}) + _, err := HandleHTTPError(&resp, receivedErr, message, ConnectionDetailsHTTP{}) assert.EqualError(t, err, fmt.Sprintf("%s", receivedErr.Error())) log.Entry().Info(err.Error()) }) @@ -361,7 +361,7 @@ func TestHandleHTTPError(t *testing.T) { _, hook := test.NewNullLogger() log.RegisterHook(hook) - err := HandleHTTPError(nil, receivedErr, message, ConnectionDetailsHTTP{}) + _, err := HandleHTTPError(nil, receivedErr, message, ConnectionDetailsHTTP{}) assert.EqualError(t, err, fmt.Sprintf("%s", receivedErr.Error())) assert.Equal(t, 5, len(hook.Entries), "Expected a different number of entries") diff --git a/pkg/abaputils/manageGitRepositoryUtils.go b/pkg/abaputils/manageGitRepositoryUtils.go index 9f3d5cc70b..19a56659f9 100644 --- a/pkg/abaputils/manageGitRepositoryUtils.go +++ b/pkg/abaputils/manageGitRepositoryUtils.go @@ -1,51 +1,48 @@ package abaputils import ( - "encoding/json" "fmt" - "io" "reflect" "sort" "strconv" "strings" "time" - piperhttp "github.com/SAP/jenkins-library/pkg/http" "github.com/SAP/jenkins-library/pkg/log" "github.com/pkg/errors" ) -const failureMessageClonePull = "Could not pull the Repository / Software Component " const numberOfEntriesPerPage = 100000 const logOutputStatusLength = 10 const logOutputTimestampLength = 29 -// PollEntity periodically polls the pull/import entity to get the status. Check if the import is still running -func PollEntity(repositoryName string, connectionDetails ConnectionDetailsHTTP, client piperhttp.Sender, pollIntervall time.Duration) (string, error) { +// PollEntity periodically polls the action entity to get the status. Check if the import is still running +func PollEntity(api SoftwareComponentApiInterface, pollIntervall time.Duration) (string, error) { log.Entry().Info("Start polling the status...") - var status string = "R" + var statusCode string = "R" + var err error for { - pullEntity, responseStatus, err := GetStatus(failureMessageClonePull+repositoryName, connectionDetails, client) + // pullEntity, responseStatus, err := api.GetStatus(failureMessageClonePull+repositoryName, connectionDetails, client) + statusCode, err = api.GetAction() if err != nil { - return status, err + return statusCode, err } - status = pullEntity.Status - log.Entry().WithField("StatusCode", responseStatus).Info("Status: " + pullEntity.Status + " - " + pullEntity.StatusDescription) - if pullEntity.Status != "R" && pullEntity.Status != "Q" { - PrintLogs(repositoryName, connectionDetails, client) + if statusCode != "R" && statusCode != "Q" { + + PrintLogs(api) break } time.Sleep(pollIntervall) } - return status, nil + return statusCode, nil } -func PrintLogs(repositoryName string, connectionDetails ConnectionDetailsHTTP, client piperhttp.Sender) { - connectionDetails.URL = connectionDetails.URL + "?$expand=to_Log_Overview" - entity, _, err := GetStatus(failureMessageClonePull+repositoryName, connectionDetails, client) +func PrintLogs(api SoftwareComponentApiInterface) { + // connectionDetails.URL = connectionDetails.URL + "?$expand=to_Log_Overview" + entity, err := api.GetLogOverview() if err != nil || len(entity.ToLogOverview.Results) == 0 { // return if no logs are available return @@ -60,14 +57,14 @@ func PrintLogs(repositoryName string, connectionDetails ConnectionDetailsHTTP, c // Print Details for _, logEntryForDetails := range entity.ToLogOverview.Results { - printLog(logEntryForDetails, connectionDetails, client) + printLog(logEntryForDetails, api) } - AddDefaultDashedLine() + AddDefaultDashedLine(1) return } -func printOverview(entity PullEntity) { +func printOverview(entity ActionEntity) { logOutputPhaseLength, logOutputLineLength := calculateLenghts(entity) @@ -85,7 +82,7 @@ func printOverview(entity PullEntity) { printDashedLine(logOutputLineLength) } -func calculateLenghts(entity PullEntity) (int, int) { +func calculateLenghts(entity ActionEntity) (int, int) { phaseLength := 22 for _, logEntry := range entity.ToLogOverview.Results { if l := len(logEntry.Name); l > phaseLength { @@ -101,24 +98,18 @@ func printDashedLine(i int) { log.Entry().Infof(strings.Repeat("-", i)) } -func printLog(logOverviewEntry LogResultsV2, connectionDetails ConnectionDetailsHTTP, client piperhttp.Sender) { +func printLog(logOverviewEntry LogResultsV2, api SoftwareComponentApiInterface) { page := 0 - printHeader(logOverviewEntry) - for { - connectionDetails.URL = logOverviewEntry.ToLogProtocol.Deferred.URI + getLogProtocolQuery(page) - entity, err := GetProtocol(failureMessageClonePull, connectionDetails, client) - - printLogProtocolEntries(logOverviewEntry, entity) - + logProtocolEntry, err := api.GetLogProtocol(logOverviewEntry, page) + printLogProtocolEntries(logOverviewEntry, logProtocolEntry) page += 1 - if allLogsHaveBeenPrinted(entity, page, err) { + if allLogsHaveBeenPrinted(logProtocolEntry, page, err) { break } } - } func printLogProtocolEntries(logEntry LogResultsV2, entity LogProtocolResults) { @@ -126,12 +117,10 @@ func printLogProtocolEntries(logEntry LogResultsV2, entity LogProtocolResults) { sort.SliceStable(entity.Results, func(i, j int) bool { return entity.Results[i].ProtocolLine < entity.Results[j].ProtocolLine }) - if logEntry.Status != `Success` { for _, entry := range entity.Results { log.Entry().Info(entry.Description) } - } else { for _, entry := range entity.Results { log.Entry().Debug(entry.Description) @@ -144,6 +133,8 @@ func allLogsHaveBeenPrinted(entity LogProtocolResults, page int, err error) bool numberOfProtocols, errConversion := strconv.Atoi(entity.Count) if errConversion == nil { allPagesHaveBeenRead = numberOfProtocols <= page*numberOfEntriesPerPage + } else { + return true } return (err != nil || allPagesHaveBeenRead || reflect.DeepEqual(entity.Results, LogProtocolResults{})) } @@ -151,9 +142,9 @@ func allLogsHaveBeenPrinted(entity LogProtocolResults, page int, err error) bool func printHeader(logEntry LogResultsV2) { if logEntry.Status != `Success` { log.Entry().Infof("\n") - AddDefaultDashedLine() + AddDefaultDashedLine(1) log.Entry().Infof("%s (%v)", logEntry.Name, ConvertTime(logEntry.Timestamp)) - AddDefaultDashedLine() + AddDefaultDashedLine(1) } else { log.Entry().Debugf("\n") AddDebugDashedLine() @@ -169,65 +160,6 @@ func getLogProtocolQuery(page int) string { return fmt.Sprintf("?$skip=%s&$top=%s&$inlinecount=allpages", fmt.Sprint(skip), fmt.Sprint(top)) } -func GetStatus(failureMessage string, connectionDetails ConnectionDetailsHTTP, client piperhttp.Sender) (body PullEntity, status string, err error) { - resp, err := GetHTTPResponse("GET", connectionDetails, nil, client) - if err != nil { - log.SetErrorCategory(log.ErrorInfrastructure) - err = HandleHTTPError(resp, err, failureMessage, connectionDetails) - if resp != nil { - status = resp.Status - } - return body, status, err - } - defer resp.Body.Close() - - // Parse response - var abapResp map[string]*json.RawMessage - bodyText, _ := io.ReadAll(resp.Body) - - marshallError := json.Unmarshal(bodyText, &abapResp) - if marshallError != nil { - return body, status, errors.Wrap(marshallError, "Could not parse response from the ABAP Environment system") - } - marshallError = json.Unmarshal(*abapResp["d"], &body) - if marshallError != nil { - return body, status, errors.Wrap(marshallError, "Could not parse response from the ABAP Environment system") - } - - if reflect.DeepEqual(PullEntity{}, body) { - log.Entry().WithField("StatusCode", resp.Status).Error(failureMessage) - log.SetErrorCategory(log.ErrorInfrastructure) - var err = errors.New("Request to ABAP System not successful") - return body, resp.Status, err - } - return body, resp.Status, nil -} - -func GetProtocol(failureMessage string, connectionDetails ConnectionDetailsHTTP, client piperhttp.Sender) (body LogProtocolResults, err error) { - resp, err := GetHTTPResponse("GET", connectionDetails, nil, client) - if err != nil { - log.SetErrorCategory(log.ErrorInfrastructure) - err = HandleHTTPError(resp, err, failureMessage, connectionDetails) - return body, err - } - defer resp.Body.Close() - - // Parse response - var abapResp map[string]*json.RawMessage - bodyText, _ := io.ReadAll(resp.Body) - - marshallError := json.Unmarshal(bodyText, &abapResp) - if marshallError != nil { - return body, errors.Wrap(marshallError, "Could not parse response from the ABAP Environment system") - } - marshallError = json.Unmarshal(*abapResp["d"], &body) - if marshallError != nil { - return body, errors.Wrap(marshallError, "Could not parse response from the ABAP Environment system") - } - - return body, nil -} - // GetRepositories for parsing one or multiple branches and repositories from repositories file or branchName and repositoryName configuration func GetRepositories(config *RepositoriesConfig, branchRequired bool) ([]Repository, error) { var repositories = make([]Repository, 0) @@ -313,118 +245,3 @@ func (repo *Repository) GetPullLogString() (logString string) { logString = "repository / software component '" + repo.Name + "'" + commitOrTag return logString } - -/**************************************** - * Structs for the A4C_A2G_GHA service * - ****************************************/ - -// PullEntity struct for the Pull/Import entity A4C_A2G_GHA_SC_IMP -type PullEntity struct { - Metadata AbapMetadata `json:"__metadata"` - UUID string `json:"uuid"` - Namespace string `json:"namepsace"` - ScName string `json:"sc_name"` - ImportType string `json:"import_type"` - BranchName string `json:"branch_name"` - StartedByUser string `json:"user_name"` - Status string `json:"status"` - StatusDescription string `json:"status_descr"` - CommitID string `json:"commit_id"` - StartTime string `json:"start_time"` - ChangeTime string `json:"change_time"` - ToExecutionLog AbapLogs `json:"to_Execution_log"` - ToTransportLog AbapLogs `json:"to_Transport_log"` - ToLogOverview AbapLogsV2 `json:"to_Log_Overview"` -} - -// BranchEntity struct for the Branch entity A4C_A2G_GHA_SC_BRANCH -type BranchEntity struct { - Metadata AbapMetadata `json:"__metadata"` - ScName string `json:"sc_name"` - Namespace string `json:"namepsace"` - BranchName string `json:"branch_name"` - ParentBranch string `json:"derived_from"` - CreatedBy string `json:"created_by"` - CreatedOn string `json:"created_on"` - IsActive bool `json:"is_active"` - CommitID string `json:"commit_id"` - CommitMessage string `json:"commit_message"` - LastCommitBy string `json:"last_commit_by"` - LastCommitOn string `json:"last_commit_on"` -} - -// CloneEntity struct for the Clone entity A4C_A2G_GHA_SC_CLONE -type CloneEntity struct { - Metadata AbapMetadata `json:"__metadata"` - UUID string `json:"uuid"` - ScName string `json:"sc_name"` - BranchName string `json:"branch_name"` - ImportType string `json:"import_type"` - Namespace string `json:"namepsace"` - Status string `json:"status"` - StatusDescription string `json:"status_descr"` - StartedByUser string `json:"user_name"` - StartTime string `json:"start_time"` - ChangeTime string `json:"change_time"` -} - -// AbapLogs struct for ABAP logs -type AbapLogs struct { - Results []LogResults `json:"results"` -} - -type AbapLogsV2 struct { - Results []LogResultsV2 `json:"results"` -} - -type LogResultsV2 struct { - Metadata AbapMetadata `json:"__metadata"` - Index int `json:"log_index"` - Name string `json:"log_name"` - Status string `json:"type_of_found_issues"` - Timestamp string `json:"timestamp"` - ToLogProtocol LogProtocolDeferred `json:"to_Log_Protocol"` -} - -type LogProtocolDeferred struct { - Deferred URI `json:"__deferred"` -} - -type URI struct { - URI string `json:"uri"` -} - -type LogProtocolResults struct { - Results []LogProtocol `json:"results"` - Count string `json:"__count"` -} - -type LogProtocol struct { - Metadata AbapMetadata `json:"__metadata"` - OverviewIndex int `json:"log_index"` - ProtocolLine int `json:"index_no"` - Type string `json:"type"` - Description string `json:"descr"` - Timestamp string `json:"timestamp"` -} - -// LogResults struct for Execution and Transport Log entities A4C_A2G_GHA_SC_LOG_EXE and A4C_A2G_GHA_SC_LOG_TP -type LogResults struct { - Index string `json:"index_no"` - Type string `json:"type"` - Description string `json:"descr"` - Timestamp string `json:"timestamp"` -} - -// RepositoriesConfig struct for parsing one or multiple branches and repositories configurations -type RepositoriesConfig struct { - BranchName string - CommitID string - RepositoryName string - RepositoryNames []string - Repositories string -} - -type EntitySetsForManageGitRepository struct { - EntitySets []string `json:"EntitySets"` -} diff --git a/pkg/abaputils/manageGitRepositoryUtils_test.go b/pkg/abaputils/manageGitRepositoryUtils_test.go index eac8ad2f41..58a37cb92d 100644 --- a/pkg/abaputils/manageGitRepositoryUtils_test.go +++ b/pkg/abaputils/manageGitRepositoryUtils_test.go @@ -10,7 +10,6 @@ import ( "os" "testing" - "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) @@ -46,33 +45,25 @@ func TestPollEntity(t *testing.T) { logResultSuccess, `{"d" : { "status" : "S" } }`, `{"d" : { "status" : "R" } }`, + `{"d" : { "status" : "Q" } }`, + `{}`, }, Token: "myToken", StatusCode: 200, } - options := AbapEnvironmentOptions{ - CfAPIEndpoint: "https://api.endpoint.com", - CfOrg: "testOrg", - CfSpace: "testSpace", - CfServiceInstance: "testInstance", - CfServiceKeyName: "testServiceKey", - Username: "testUser", - Password: "testPassword", - } - - config := AbapEnvironmentCheckoutBranchOptions{ - AbapEnvOptions: options, - RepositoryName: "testRepo1", - } - con := ConnectionDetailsHTTP{ User: "MY_USER", Password: "MY_PW", URL: "https://api.endpoint.com/Entity/", XCsrfToken: "MY_TOKEN", } - status, _ := PollEntity(config.RepositoryName, con, client, 0) + + swcManager := SoftwareComponentApiManager{Client: client} + repo := Repository{Name: "testRepo1"} + api, _ := swcManager.GetAPI(con, repo) + + status, _ := PollEntity(api, 0) assert.Equal(t, "S", status) assert.Equal(t, 0, len(client.BodyList), "Not all requests were done") }) @@ -87,33 +78,24 @@ func TestPollEntity(t *testing.T) { `{"d" : { "status" : "E" } }`, `{"d" : { "status" : "R" } }`, `{"d" : { "status" : "Q" } }`, + `{}`, }, Token: "myToken", StatusCode: 200, } - options := AbapEnvironmentOptions{ - CfAPIEndpoint: "https://api.endpoint.com", - CfOrg: "testOrg", - CfSpace: "testSpace", - CfServiceInstance: "testInstance", - CfServiceKeyName: "testServiceKey", - Username: "testUser", - Password: "testPassword", - } - - config := AbapEnvironmentCheckoutBranchOptions{ - AbapEnvOptions: options, - RepositoryName: "testRepo1", - } - con := ConnectionDetailsHTTP{ User: "MY_USER", Password: "MY_PW", URL: "https://api.endpoint.com/Entity/", XCsrfToken: "MY_TOKEN", } - status, _ := PollEntity(config.RepositoryName, con, client, 0) + + swcManager := SoftwareComponentApiManager{Client: client} + repo := Repository{Name: "testRepo1"} + api, _ := swcManager.GetAPI(con, repo) + + status, _ := PollEntity(api, 0) assert.Equal(t, "E", status) assert.Equal(t, 0, len(client.BodyList), "Not all requests were done") }) @@ -318,22 +300,3 @@ func TestCreateRequestBodies(t *testing.T) { assert.Equal(t, `{"sc_name":"/DMO/REPO", "tag_name":"myTag"}`, body, "Expected different body") }) } - -func TestGetStatus(t *testing.T) { - t.Run("Graceful Exit", func(t *testing.T) { - - client := &ClientMock{ - NilResponse: true, - Error: errors.New("Backend Error"), - StatusCode: 500, - } - connectionDetails := ConnectionDetailsHTTP{ - URL: "example.com", - } - - _, status, err := GetStatus("failure message", connectionDetails, client) - - assert.Error(t, err, "Expected Error") - assert.Equal(t, "", status) - }) -} diff --git a/pkg/abaputils/sap_com_0510.go b/pkg/abaputils/sap_com_0510.go new file mode 100644 index 0000000000..c7940832d5 --- /dev/null +++ b/pkg/abaputils/sap_com_0510.go @@ -0,0 +1,369 @@ +package abaputils + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/cookiejar" + "reflect" + "strings" + "time" + + piperhttp "github.com/SAP/jenkins-library/pkg/http" + "github.com/SAP/jenkins-library/pkg/log" + "github.com/pkg/errors" + "k8s.io/utils/strings/slices" +) + +type SAP_COM_0510 struct { + con ConnectionDetailsHTTP + client piperhttp.Sender + repository Repository + path string + cloneEntity string + repositoryEntity string + tagsEntity string + checkoutAction string + actionEntity string + uuid string + failureMessage string + maxRetries int + retryBaseSleepUnit time.Duration + retryMaxSleepTime time.Duration + retryAllowedErrorCodes []string +} + +func (api *SAP_COM_0510) init(con ConnectionDetailsHTTP, client piperhttp.Sender, repo Repository) { + api.con = con + api.client = client + api.repository = repo + api.path = "/sap/opu/odata/sap/MANAGE_GIT_REPOSITORY" + api.cloneEntity = "/Clones" + api.repositoryEntity = "/Repositories" + api.tagsEntity = "/Tags" + api.actionEntity = "/Pull" + api.checkoutAction = "/checkout_branch" + api.failureMessage = "The action of the Repository / Software Component " + api.repository.Name + " failed" + api.maxRetries = 3 + api.setSleepTimeConfig(1*time.Second, 120*time.Second) + api.retryAllowedErrorCodes = append(api.retryAllowedErrorCodes, "A4C_A2G/228") +} + +func (api *SAP_COM_0510) getUUID() string { + return api.uuid +} + +func (api *SAP_COM_0510) CreateTag(tag Tag) error { + + if reflect.DeepEqual(Tag{}, tag) { + return errors.New("No Tag provided") + } + + con := api.con + con.URL = api.con.URL + api.path + api.tagsEntity + + requestBodyStruct := CreateTagBody{RepositoryName: api.repository.Name, CommitID: api.repository.CommitID, Tag: tag.TagName, Description: tag.TagDescription} + jsonBody, err := json.Marshal(&requestBodyStruct) + if err != nil { + return err + } + return api.triggerRequest(con, jsonBody) +} + +func (api *SAP_COM_0510) CheckoutBranch() error { + + if api.repository.Name == "" || api.repository.Branch == "" { + return fmt.Errorf("Failed to trigger checkout: %w", errors.New("Repository and/or Branch Configuration is empty. Please make sure that you have specified the correct values")) + } + + // the request looks like: POST/sap/opu/odata/sap/MANAGE_GIT_REPOSITORY/checkout_branch?branch_name='newBranch'&sc_name=/DMO/GIT_REPOSITORY' + checkoutConnectionDetails := api.con + checkoutConnectionDetails.URL = api.con.URL + api.path + api.checkoutAction + `?branch_name='` + api.repository.Branch + `'&sc_name='` + api.repository.Name + `'` + jsonBody := []byte(``) + + return api.triggerRequest(checkoutConnectionDetails, jsonBody) +} + +func (api *SAP_COM_0510) parseActionResponse(resp *http.Response, err error) (ActionEntity, error) { + var body ActionEntity + var abapResp map[string]*json.RawMessage + bodyText, errRead := io.ReadAll(resp.Body) + if errRead != nil { + return ActionEntity{}, err + } + if err := json.Unmarshal(bodyText, &abapResp); err != nil { + return ActionEntity{}, err + } + if err := json.Unmarshal(*abapResp["d"], &body); err != nil { + return ActionEntity{}, err + } + + if reflect.DeepEqual(ActionEntity{}, body) { + log.Entry().WithField("StatusCode", resp.Status).WithField("branchName", api.repository.Branch).Error("Could not switch to specified branch") + err := errors.New("Request to ABAP System not successful") + return ActionEntity{}, err + } + return body, nil +} + +func (api *SAP_COM_0510) Pull() error { + + // Trigger the Pull of a Repository + if api.repository.Name == "" { + return errors.New("An empty string was passed for the parameter 'repositoryName'") + } + + pullConnectionDetails := api.con + pullConnectionDetails.URL = api.con.URL + api.path + api.actionEntity + + jsonBody := []byte(api.repository.GetPullRequestBody()) + return api.triggerRequest(pullConnectionDetails, jsonBody) +} + +func (api *SAP_COM_0510) GetLogProtocol(logOverviewEntry LogResultsV2, page int) (body LogProtocolResults, err error) { + + connectionDetails := api.con + connectionDetails.URL = logOverviewEntry.ToLogProtocol.Deferred.URI + getLogProtocolQuery(page) + resp, err := GetHTTPResponse("GET", connectionDetails, nil, api.client) + if err != nil { + log.SetErrorCategory(log.ErrorInfrastructure) + _, err = HandleHTTPError(resp, err, api.failureMessage, connectionDetails) + return body, err + } + defer resp.Body.Close() + + // Parse response + var abapResp map[string]*json.RawMessage + bodyText, _ := io.ReadAll(resp.Body) + + marshallError := json.Unmarshal(bodyText, &abapResp) + if marshallError != nil { + return body, errors.Wrap(marshallError, "Could not parse response from the ABAP Environment system") + } + marshallError = json.Unmarshal(*abapResp["d"], &body) + if marshallError != nil { + return body, errors.Wrap(marshallError, "Could not parse response from the ABAP Environment system") + } + + return body, nil +} + +func (api *SAP_COM_0510) GetLogOverview() (body ActionEntity, err error) { + + connectionDetails := api.con + connectionDetails.URL = api.con.URL + api.path + api.actionEntity + "(uuid=guid'" + api.getUUID() + "')" + "?$expand=to_Log_Overview" + resp, err := GetHTTPResponse("GET", connectionDetails, nil, api.client) + if err != nil { + log.SetErrorCategory(log.ErrorInfrastructure) + _, err = HandleHTTPError(resp, err, api.failureMessage, connectionDetails) + return body, err + } + defer resp.Body.Close() + + // Parse response + var abapResp map[string]*json.RawMessage + bodyText, _ := io.ReadAll(resp.Body) + + marshallError := json.Unmarshal(bodyText, &abapResp) + if marshallError != nil { + return body, errors.Wrap(marshallError, "Could not parse response from the ABAP Environment system") + } + marshallError = json.Unmarshal(*abapResp["d"], &body) + if marshallError != nil { + return body, errors.Wrap(marshallError, "Could not parse response from the ABAP Environment system") + } + + if reflect.DeepEqual(ActionEntity{}, body) { + log.Entry().WithField("StatusCode", resp.Status).Error(api.failureMessage) + log.SetErrorCategory(log.ErrorInfrastructure) + var err = errors.New("Request to ABAP System not successful") + return body, err + } + + abapStatusCode := body.Status + log.Entry().Info("Status: " + abapStatusCode + " - " + body.StatusDescription) + return body, nil + +} + +func (api *SAP_COM_0510) GetAction() (string, error) { + + connectionDetails := api.con + connectionDetails.URL = api.con.URL + api.path + api.actionEntity + "(uuid=guid'" + api.getUUID() + "')" + resp, err := GetHTTPResponse("GET", connectionDetails, nil, api.client) + if err != nil { + log.SetErrorCategory(log.ErrorInfrastructure) + _, err = HandleHTTPError(resp, err, api.failureMessage, connectionDetails) + return "E", err + } + defer resp.Body.Close() + + // Parse Response + body, parseError := api.parseActionResponse(resp, err) + if parseError != nil { + return "E", parseError + } + + api.uuid = body.UUID + + abapStatusCode := body.Status + log.Entry().Info("Status: " + abapStatusCode + " - " + body.StatusDescription) + return abapStatusCode, nil +} + +func (api *SAP_COM_0510) GetRepository() (bool, string, error) { + + if api.repository.Name == "" { + return false, "", errors.New("An empty string was passed for the parameter 'repositoryName'") + } + + swcConnectionDetails := api.con + swcConnectionDetails.URL = api.con.URL + api.path + api.repositoryEntity + "('" + strings.Replace(api.repository.Name, "/", "%2F", -1) + "')" + resp, err := GetHTTPResponse("GET", swcConnectionDetails, nil, api.client) + if err != nil { + _, errRepo := HandleHTTPError(resp, err, "Reading the Repository / Software Component failed", api.con) + return false, "", errRepo + } + defer resp.Body.Close() + + var body RepositoryEntity + var abapResp map[string]*json.RawMessage + bodyText, errRead := io.ReadAll(resp.Body) + if errRead != nil { + return false, "", err + } + + if err := json.Unmarshal(bodyText, &abapResp); err != nil { + return false, "", err + } + if err := json.Unmarshal(*abapResp["d"], &body); err != nil { + return false, "", err + } + if reflect.DeepEqual(RepositoryEntity{}, body) { + log.Entry().WithField("StatusCode", resp.Status).WithField("repositoryName", api.repository.Name).WithField("branchName", api.repository.Branch).WithField("commitID", api.repository.CommitID).WithField("Tag", api.repository.Tag).Error("Could not Clone the Repository / Software Component") + err := errors.New("Request to ABAP System not successful") + return false, "", err + } + + if body.AvailOnInst { + return true, body.ActiveBranch, nil + } + return false, "", err + +} + +func (api *SAP_COM_0510) Clone() error { + + // Trigger the Clone of a Repository + if api.repository.Name == "" { + return errors.New("An empty string was passed for the parameter 'repositoryName'") + } + + cloneConnectionDetails := api.con + cloneConnectionDetails.URL = api.con.URL + api.path + api.cloneEntity + body := []byte(api.repository.GetCloneRequestBody()) + + return api.triggerRequest(cloneConnectionDetails, body) + +} + +func (api *SAP_COM_0510) triggerRequest(cloneConnectionDetails ConnectionDetailsHTTP, jsonBody []byte) error { + var err error + var body ActionEntity + var resp *http.Response + var errorCode string + + for i := 0; i <= api.maxRetries; i++ { + if i > 0 { + sleepTime, err := api.getSleepTime(i + 5) + if err != nil { + // reached max retry duration + break + } + log.Entry().Infof("Retrying in %s", sleepTime.String()) + time.Sleep(sleepTime) + } + resp, err = GetHTTPResponse("POST", cloneConnectionDetails, jsonBody, api.client) + if err != nil { + errorCode, err = HandleHTTPError(resp, err, "Triggering the action failed", api.con) + if slices.Contains(api.retryAllowedErrorCodes, errorCode) { + // Error Code allows for retry + continue + } else { + break + } + } + defer resp.Body.Close() + log.Entry().WithField("StatusCode", resp.Status).WithField("repositoryName", api.repository.Name).WithField("branchName", api.repository.Branch).WithField("commitID", api.repository.CommitID).WithField("Tag", api.repository.Tag).Info("Triggered action of Repository / Software Component") + + body, err = api.parseActionResponse(resp, err) + break + } + api.uuid = body.UUID + return err +} + +// initialRequest implements SoftwareComponentApiInterface. +func (api *SAP_COM_0510) initialRequest() error { + // Configuring the HTTP Client and CookieJar + cookieJar, errorCookieJar := cookiejar.New(nil) + if errorCookieJar != nil { + return errors.Wrap(errorCookieJar, "Could not create a Cookie Jar") + } + + api.client.SetOptions(piperhttp.ClientOptions{ + MaxRequestDuration: 180 * time.Second, + CookieJar: cookieJar, + Username: api.con.User, + Password: api.con.Password, + }) + + headConnection := api.con + headConnection.XCsrfToken = "fetch" + headConnection.URL = api.con.URL + api.path + + // Loging into the ABAP System - getting the x-csrf-token and cookies + resp, err := GetHTTPResponse("HEAD", headConnection, nil, api.client) + if err != nil { + _, err = HandleHTTPError(resp, err, "Authentication on the ABAP system failed", api.con) + return err + } + defer resp.Body.Close() + + log.Entry().WithField("StatusCode", resp.Status).WithField("ABAP Endpoint", api.con).Debug("Authentication on the ABAP system successful") + api.con.XCsrfToken = resp.Header.Get("X-Csrf-Token") + return nil +} + +// getSleepTime Should return the Fibonacci numbers in the define time unit up to the defined maximum duration +func (api *SAP_COM_0510) getSleepTime(n int) (time.Duration, error) { + + if n == 0 { + return 0, nil + } else if n == 1 { + return 1 * api.retryBaseSleepUnit, nil + } else if n < 0 { + return 0, errors.New("Negative numbers are not allowed") + } + var result, i int + prev := 0 + next := 1 + for i = 2; i <= n; i++ { + result = prev + next + prev = next + next = result + } + sleepTime := time.Duration(result) * api.retryBaseSleepUnit + + if sleepTime > api.retryMaxSleepTime { + return 0, errors.New("Exceeded max sleep time") + } + return sleepTime, nil +} + +// setSleepTimeConfig sets the time unit (seconds, nanoseconds) and the maximum sleep duration +func (api *SAP_COM_0510) setSleepTimeConfig(timeUnit time.Duration, maxSleepTime time.Duration) { + api.retryBaseSleepUnit = timeUnit + api.retryMaxSleepTime = maxSleepTime +} diff --git a/pkg/abaputils/sap_com_0510_test.go b/pkg/abaputils/sap_com_0510_test.go new file mode 100644 index 0000000000..772207d5eb --- /dev/null +++ b/pkg/abaputils/sap_com_0510_test.go @@ -0,0 +1,483 @@ +//go:build unit +// +build unit + +package abaputils + +import ( + "testing" + "time" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +var con ConnectionDetailsHTTP +var repo Repository + +func init() { + + con.User = "CC_USER" + con.Password = "123abc" + con.URL = "https://example.com" + + repo.Name = "/DMO/REPO" + repo.Branch = "main" + +} + +func TestRetry(t *testing.T) { + t.Run("Test retry success", func(t *testing.T) { + + client := &ClientMock{ + BodyList: []string{ + `{"d" : { "status" : "R", "UUID" : "GUID" } }`, + `{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Software component lifecycle activities in progress. Try again later..."} } }`, + `{ }`, + }, + Token: "myToken", + StatusCode: 200, + ErrorList: []error{ + nil, + errors.New("HTTP 400"), + nil, + }, + } + + apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + + api, err := apiManager.GetAPI(con, repo) + api.setSleepTimeConfig(time.Nanosecond, 120*time.Nanosecond) + assert.NoError(t, err) + assert.IsType(t, &SAP_COM_0510{}, api.(*SAP_COM_0510), "API has wrong type") + + errAction := api.(*SAP_COM_0510).triggerRequest(ConnectionDetailsHTTP{User: "CC_USER", Password: "abc123", URL: "https://example.com/path"}, []byte("{}")) + assert.NoError(t, errAction) + assert.Equal(t, "GUID", api.getUUID(), "API does not cotain correct UUID") + + }) + + t.Run("Test retry not allowed", func(t *testing.T) { + + client := &ClientMock{ + BodyList: []string{ + `{"d" : { "status" : "R", "UUID" : "GUID" } }`, + `{"error" : { "code" : "A4C_A2G/224", "message" : { "lang" : "de", "value" : "Error Text"} } }`, + `{ }`, + }, + Token: "myToken", + StatusCode: 200, + ErrorList: []error{ + nil, + errors.New("HTTP 400"), + nil, + }, + } + + apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + + api, err := apiManager.GetAPI(con, repo) + api.setSleepTimeConfig(time.Nanosecond, 120*time.Nanosecond) + assert.NoError(t, err) + assert.IsType(t, &SAP_COM_0510{}, api.(*SAP_COM_0510), "API has wrong type") + + errAction := api.(*SAP_COM_0510).triggerRequest(ConnectionDetailsHTTP{User: "CC_USER", Password: "abc123", URL: "https://example.com/path"}, []byte("{}")) + assert.ErrorContains(t, errAction, "HTTP 400: A4C_A2G/224 - Error Text") + assert.Empty(t, api.getUUID(), "API does not cotain correct UUID") + + }) + + t.Run("Test retry maxSleepTime", func(t *testing.T) { + + client := &ClientMock{ + BodyList: []string{ + `{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`, + `{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`, + `{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`, + `{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`, + `{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`, + `{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`, + `{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`, + `{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`, + `{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`, + `{ }`, + }, + Token: "myToken", + StatusCode: 200, + ErrorList: []error{ + errors.New("HTTP 400"), + errors.New("HTTP 400"), + errors.New("HTTP 400"), + errors.New("HTTP 400"), + errors.New("HTTP 400"), + errors.New("HTTP 400"), + errors.New("HTTP 400"), + errors.New("HTTP 400"), + errors.New("HTTP 400"), + nil, + }, + } + + apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + + api, err := apiManager.GetAPI(con, repo) + api.setSleepTimeConfig(time.Nanosecond, 20*time.Nanosecond) + assert.NoError(t, err) + assert.IsType(t, &SAP_COM_0510{}, api.(*SAP_COM_0510), "API has wrong type") + + api.(*SAP_COM_0510).maxRetries = 20 + + errAction := api.(*SAP_COM_0510).triggerRequest(ConnectionDetailsHTTP{User: "CC_USER", Password: "abc123", URL: "https://example.com/path"}, []byte("{}")) + assert.ErrorContains(t, errAction, "HTTP 400: A4C_A2G/228 - Error Text") + assert.Empty(t, api.getUUID(), "API does not cotain correct UUID") + + assert.Equal(t, 6, len(client.BodyList), "Expected maxSleepTime to limit requests") + }) + + t.Run("Test retry maxRetries", func(t *testing.T) { + + client := &ClientMock{ + BodyList: []string{ + `{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`, + `{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`, + `{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`, + `{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`, + `{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`, + `{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`, + `{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`, + `{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`, + `{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`, + `{ }`, + }, + Token: "myToken", + StatusCode: 200, + ErrorList: []error{ + errors.New("HTTP 400"), + errors.New("HTTP 400"), + errors.New("HTTP 400"), + errors.New("HTTP 400"), + errors.New("HTTP 400"), + errors.New("HTTP 400"), + errors.New("HTTP 400"), + errors.New("HTTP 400"), + errors.New("HTTP 400"), + nil, + }, + } + + apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + + api, err := apiManager.GetAPI(con, repo) + api.setSleepTimeConfig(time.Nanosecond, 999*time.Nanosecond) + assert.NoError(t, err) + assert.IsType(t, &SAP_COM_0510{}, api.(*SAP_COM_0510), "API has wrong type") + + api.(*SAP_COM_0510).maxRetries = 3 + + errAction := api.(*SAP_COM_0510).triggerRequest(ConnectionDetailsHTTP{User: "CC_USER", Password: "abc123", URL: "https://example.com/path"}, []byte("{}")) + assert.ErrorContains(t, errAction, "HTTP 400: A4C_A2G/228 - Error Text") + assert.Empty(t, api.getUUID(), "API does not cotain correct UUID") + + assert.Equal(t, 5, len(client.BodyList), "Expected maxRetries to limit requests") + }) + +} +func TestClone(t *testing.T) { + t.Run("Test Clone Success", func(t *testing.T) { + + client := &ClientMock{ + BodyList: []string{ + `{"d" : { "status" : "R", "UUID" : "GUID" } }`, + `{ }`, + }, + Token: "myToken", + StatusCode: 200, + } + + apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + + api, err := apiManager.GetAPI(con, repo) + assert.NoError(t, err) + assert.IsType(t, &SAP_COM_0510{}, api.(*SAP_COM_0510), "API has wrong type") + + errClone := api.Clone() + assert.NoError(t, errClone) + assert.Equal(t, "GUID", api.getUUID(), "API does not cotain correct UUID") + }) + + t.Run("Test Clone Failure", func(t *testing.T) { + + client := &ClientMock{ + BodyList: []string{ + `{ "d" : {} }`, + `{ "d" : {} }`, + `{ }`, + }, + Token: "myToken", + StatusCode: 200, + } + + apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + + api, err := apiManager.GetAPI(con, repo) + api.setSleepTimeConfig(time.Nanosecond, 120*time.Nanosecond) + assert.NoError(t, err) + assert.IsType(t, &SAP_COM_0510{}, api.(*SAP_COM_0510), "API has wrong type") + + errClone := api.Clone() + assert.ErrorContains(t, errClone, "Request to ABAP System not successful") + assert.Empty(t, api.getUUID(), "API does not cotain correct UUID") + }) + + t.Run("Test Clone Retry", func(t *testing.T) { + + client := &ClientMock{ + BodyList: []string{ + `{"d" : { "status" : "R", "UUID" : "GUID" } }`, + `{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Software component lifecycle activities in progress. Try again later..."} } }`, + `{ }`, + }, + Token: "myToken", + StatusCode: 200, + ErrorList: []error{ + nil, + errors.New("HTTP 400"), + nil, + }, + } + + apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + + api, err := apiManager.GetAPI(con, repo) + api.setSleepTimeConfig(time.Nanosecond, 120*time.Nanosecond) + assert.NoError(t, err) + assert.IsType(t, &SAP_COM_0510{}, api.(*SAP_COM_0510), "API has wrong type") + + errClone := api.Clone() + assert.NoError(t, errClone) + assert.Equal(t, "GUID", api.getUUID(), "API does not cotain correct UUID") + }) +} + +func TestPull(t *testing.T) { + t.Run("Test Pull Success", func(t *testing.T) { + + client := &ClientMock{ + BodyList: []string{ + `{"d" : { "status" : "R", "UUID" : "GUID" } }`, + `{ }`, + }, + Token: "myToken", + StatusCode: 200, + } + + apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + + api, err := apiManager.GetAPI(con, repo) + assert.NoError(t, err) + assert.IsType(t, &SAP_COM_0510{}, api.(*SAP_COM_0510), "API has wrong type") + + errPull := api.Pull() + assert.NoError(t, errPull) + assert.Equal(t, "GUID", api.getUUID(), "API does not cotain correct UUID") + }) + + t.Run("Test Pull Failure", func(t *testing.T) { + + client := &ClientMock{ + BodyList: []string{ + `{ "d" : {} }`, + `{ }`, + }, + Token: "myToken", + StatusCode: 200, + } + + apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + + api, err := apiManager.GetAPI(con, repo) + assert.NoError(t, err) + assert.IsType(t, &SAP_COM_0510{}, api.(*SAP_COM_0510), "API has wrong type") + + errPull := api.Pull() + assert.ErrorContains(t, errPull, "Request to ABAP System not successful") + assert.Empty(t, api.getUUID(), "API does not cotain correct UUID") + }) +} + +func TestCheckout(t *testing.T) { + t.Run("Test Checkout Success", func(t *testing.T) { + + client := &ClientMock{ + BodyList: []string{ + `{"d" : { "status" : "R", "UUID" : "GUID" } }`, + `{ }`, + }, + Token: "myToken", + StatusCode: 200, + } + + apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + + api, err := apiManager.GetAPI(con, repo) + assert.NoError(t, err) + assert.IsType(t, &SAP_COM_0510{}, api.(*SAP_COM_0510), "API has wrong type") + + errCheckout := api.CheckoutBranch() + assert.NoError(t, errCheckout) + assert.Equal(t, "GUID", api.getUUID(), "API does not cotain correct UUID") + }) + + t.Run("Test Checkout Failure", func(t *testing.T) { + + client := &ClientMock{ + BodyList: []string{ + `{ "d" : {} }`, + `{ }`, + }, + Token: "myToken", + StatusCode: 200, + } + + apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + + api, err := apiManager.GetAPI(con, repo) + assert.NoError(t, err) + assert.IsType(t, &SAP_COM_0510{}, api.(*SAP_COM_0510), "API has wrong type") + + errCheckoput := api.CheckoutBranch() + assert.ErrorContains(t, errCheckoput, "Request to ABAP System not successful") + assert.Empty(t, api.getUUID(), "API does not cotain correct UUID") + }) +} + +func TestGetRepo(t *testing.T) { + t.Run("Test GetRepo Success", func(t *testing.T) { + + client := &ClientMock{ + BodyList: []string{ + `{"d" : { "sc_name" : "testRepo1", "avail_on_inst" : true, "active_branch": "testBranch1" } }`, + `{"d" : [] }`, + }, + Token: "myToken", + StatusCode: 200, + } + + apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + + api, err := apiManager.GetAPI(con, repo) + assert.NoError(t, err) + assert.IsType(t, &SAP_COM_0510{}, api.(*SAP_COM_0510), "API has wrong type") + + cloned, activeBranch, errAction := api.GetRepository() + assert.True(t, cloned) + assert.Equal(t, "testBranch1", activeBranch) + assert.NoError(t, errAction) + }) +} + +func TestCreateTag(t *testing.T) { + t.Run("Test Tag Success", func(t *testing.T) { + + client := &ClientMock{ + BodyList: []string{ + `{"d" : { "status" : "R", "UUID" : "GUID" } }`, + `{ }`, + }, + Token: "myToken", + StatusCode: 200, + } + + apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + + api, err := apiManager.GetAPI(con, repo) + assert.NoError(t, err) + assert.IsType(t, &SAP_COM_0510{}, api.(*SAP_COM_0510), "API has wrong type") + + errCreateTag := api.CreateTag(Tag{TagName: "myTag", TagDescription: "descr"}) + assert.NoError(t, errCreateTag) + assert.Equal(t, "GUID", api.getUUID(), "API does not cotain correct UUID") + }) + + t.Run("Test Tag Failure", func(t *testing.T) { + + client := &ClientMock{ + BodyList: []string{ + `{ "d" : {} }`, + `{ }`, + }, + Token: "myToken", + StatusCode: 200, + } + + apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + + api, err := apiManager.GetAPI(con, repo) + assert.NoError(t, err) + assert.IsType(t, &SAP_COM_0510{}, api.(*SAP_COM_0510), "API has wrong type") + + errCreateTag := api.CreateTag(Tag{TagName: "myTag", TagDescription: "descr"}) + assert.ErrorContains(t, errCreateTag, "Request to ABAP System not successful") + assert.Empty(t, api.getUUID(), "API does not cotain correct UUID") + }) + + t.Run("Test Tag Empty", func(t *testing.T) { + + client := &ClientMock{ + BodyList: []string{ + `{ "d" : {} }`, + `{ }`, + }, + Token: "myToken", + StatusCode: 200, + } + + apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond} + + api, err := apiManager.GetAPI(con, repo) + assert.NoError(t, err) + assert.IsType(t, &SAP_COM_0510{}, api.(*SAP_COM_0510), "API has wrong type") + + errCreateTag := api.CreateTag(Tag{}) + assert.ErrorContains(t, errCreateTag, "No Tag provided") + assert.Empty(t, api.getUUID(), "API does not cotain correct UUID") + }) +} + +func TestSleepTime(t *testing.T) { + t.Run("Test Sleep Time", func(t *testing.T) { + + api := SAP_COM_0510{ + retryMaxSleepTime: 120 * time.Nanosecond, + retryBaseSleepUnit: 1 * time.Nanosecond, + } + + expectedResults := make([]time.Duration, 12) + expectedResults[0] = 0 + expectedResults[1] = 1 + expectedResults[2] = 1 + expectedResults[3] = 2 + expectedResults[4] = 3 + expectedResults[5] = 5 + expectedResults[6] = 8 + expectedResults[7] = 13 + expectedResults[8] = 21 + expectedResults[9] = 34 + expectedResults[10] = 55 + expectedResults[11] = 89 + results := make([]time.Duration, 12) + var err error + + for i := 0; i <= 11; i++ { + + results[i], err = api.getSleepTime(i) + assert.NoError(t, err) + } + assert.ElementsMatch(t, expectedResults, results) + + _, err = api.getSleepTime(-10) + assert.Error(t, err) + + _, err = api.getSleepTime(12) + assert.ErrorContains(t, err, "Exceeded max sleep time") + }) +} diff --git a/pkg/abaputils/softwareComponentApiManager.go b/pkg/abaputils/softwareComponentApiManager.go new file mode 100644 index 0000000000..65565a0405 --- /dev/null +++ b/pkg/abaputils/softwareComponentApiManager.go @@ -0,0 +1,194 @@ +package abaputils + +import ( + "time" + + piperhttp "github.com/SAP/jenkins-library/pkg/http" +) + +type SoftwareComponentApiManagerInterface interface { + GetAPI(con ConnectionDetailsHTTP, repo Repository) (SoftwareComponentApiInterface, error) + GetPollIntervall() time.Duration +} + +type SoftwareComponentApiManager struct { + Client piperhttp.Sender + PollIntervall time.Duration +} + +func (manager *SoftwareComponentApiManager) GetAPI(con ConnectionDetailsHTTP, repo Repository) (SoftwareComponentApiInterface, error) { + sap_com_0510 := SAP_COM_0510{} + sap_com_0510.init(con, manager.Client, repo) + + // Initialize all APIs, use the one that returns a response + // Currently SAP_COM_0510, later SAP_COM_0948 + err := sap_com_0510.initialRequest() + return &sap_com_0510, err +} + +func (manager *SoftwareComponentApiManager) GetPollIntervall() time.Duration { + if manager.PollIntervall == 0 { + manager.PollIntervall = 5 * time.Second + } + return manager.PollIntervall +} + +type SoftwareComponentApiInterface interface { + init(con ConnectionDetailsHTTP, client piperhttp.Sender, repo Repository) + initialRequest() error + setSleepTimeConfig(timeUnit time.Duration, maxSleepTime time.Duration) + getSleepTime(n int) (time.Duration, error) + getUUID() string + Clone() error + Pull() error + CheckoutBranch() error + GetRepository() (bool, string, error) + GetAction() (string, error) + GetLogOverview() (ActionEntity, error) + GetLogProtocol(LogResultsV2, int) (body LogProtocolResults, err error) + CreateTag(tag Tag) error +} + +/**************************************** + * Structs for the A4C_A2G_GHA service * + ****************************************/ + +// ActionEntity struct for the Pull/Import entity A4C_A2G_GHA_SC_IMP +type ActionEntity struct { + Metadata AbapMetadata `json:"__metadata"` + UUID string `json:"uuid"` + Namespace string `json:"namepsace"` + ScName string `json:"sc_name"` + ImportType string `json:"import_type"` + BranchName string `json:"branch_name"` + StartedByUser string `json:"user_name"` + Status string `json:"status"` + StatusDescription string `json:"status_descr"` + CommitID string `json:"commit_id"` + StartTime string `json:"start_time"` + ChangeTime string `json:"change_time"` + ToExecutionLog AbapLogs `json:"to_Execution_log"` + ToTransportLog AbapLogs `json:"to_Transport_log"` + ToLogOverview AbapLogsV2 `json:"to_Log_Overview"` +} + +// BranchEntity struct for the Branch entity A4C_A2G_GHA_SC_BRANCH +type BranchEntity struct { + Metadata AbapMetadata `json:"__metadata"` + ScName string `json:"sc_name"` + Namespace string `json:"namepsace"` + BranchName string `json:"branch_name"` + ParentBranch string `json:"derived_from"` + CreatedBy string `json:"created_by"` + CreatedOn string `json:"created_on"` + IsActive bool `json:"is_active"` + CommitID string `json:"commit_id"` + CommitMessage string `json:"commit_message"` + LastCommitBy string `json:"last_commit_by"` + LastCommitOn string `json:"last_commit_on"` +} + +// CloneEntity struct for the Clone entity A4C_A2G_GHA_SC_CLONE +type CloneEntity struct { + Metadata AbapMetadata `json:"__metadata"` + UUID string `json:"uuid"` + ScName string `json:"sc_name"` + BranchName string `json:"branch_name"` + ImportType string `json:"import_type"` + Namespace string `json:"namepsace"` + Status string `json:"status"` + StatusDescription string `json:"status_descr"` + StartedByUser string `json:"user_name"` + StartTime string `json:"start_time"` + ChangeTime string `json:"change_time"` +} + +type RepositoryEntity struct { + Metadata AbapMetadata `json:"__metadata"` + ScName string `json:"sc_name"` + ActiveBranch string `json:"active_branch"` + AvailOnInst bool `json:"avail_on_inst"` +} + +// AbapLogs struct for ABAP logs +type AbapLogs struct { + Results []LogResults `json:"results"` +} + +type AbapLogsV2 struct { + Results []LogResultsV2 `json:"results"` +} + +type LogResultsV2 struct { + Metadata AbapMetadata `json:"__metadata"` + Index int `json:"log_index"` + Name string `json:"log_name"` + Status string `json:"type_of_found_issues"` + Timestamp string `json:"timestamp"` + ToLogProtocol LogProtocolDeferred `json:"to_Log_Protocol"` +} + +type LogProtocolDeferred struct { + Deferred URI `json:"__deferred"` +} + +type URI struct { + URI string `json:"uri"` +} + +type LogProtocolResults struct { + Results []LogProtocol `json:"results"` + Count string `json:"__count"` +} + +type LogProtocol struct { + Metadata AbapMetadata `json:"__metadata"` + OverviewIndex int `json:"log_index"` + ProtocolLine int `json:"index_no"` + Type string `json:"type"` + Description string `json:"descr"` + Timestamp string `json:"timestamp"` +} + +// LogResults struct for Execution and Transport Log entities A4C_A2G_GHA_SC_LOG_EXE and A4C_A2G_GHA_SC_LOG_TP +type LogResults struct { + Index string `json:"index_no"` + Type string `json:"type"` + Description string `json:"descr"` + Timestamp string `json:"timestamp"` +} + +// RepositoriesConfig struct for parsing one or multiple branches and repositories configurations +type RepositoriesConfig struct { + BranchName string + CommitID string + RepositoryName string + RepositoryNames []string + Repositories string +} + +type EntitySetsForManageGitRepository struct { + EntitySets []string `json:"EntitySets"` +} + +type CreateTagBacklog struct { + RepositoryName string + CommitID string + Tags []Tag +} + +type Tag struct { + TagName string + TagDescription string +} + +type CreateTagBody struct { + RepositoryName string `json:"sc_name"` + CommitID string `json:"commit_id"` + Tag string `json:"tag_name"` + Description string `json:"tag_description"` +} + +type CreateTagResponse struct { + UUID string `json:"uuid"` +} From 2738a910572deb85bf46f485545079753274500a Mon Sep 17 00:00:00 2001 From: Silvestre Zabala Date: Wed, 29 Nov 2023 10:23:38 +0100 Subject: [PATCH 7/8] Fix logic of fetching golang private packages for `detectExecute step (#4695) In #4595 a typo was committed that prevents Go private packages from being correctly set up in the `detectExecute` step Co-authored-by: Anil Keshav --- cmd/detectExecuteScan.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/detectExecuteScan.go b/cmd/detectExecuteScan.go index eeb5099181..7c77192df3 100644 --- a/cmd/detectExecuteScan.go +++ b/cmd/detectExecuteScan.go @@ -140,7 +140,7 @@ func detectExecuteScan(config detectExecuteScanOptions, _ *telemetry.CustomData, log.Entry().WithError(err).Warning("Failed to get GitHub client") } - if config.PrivateModules == "" && config.PrivateModulesGitToken != "" { + if config.PrivateModules != "" && config.PrivateModulesGitToken != "" { //configuring go private packages if err := golang.PrepareGolangPrivatePackages("detectExecuteStep", config.PrivateModules, config.PrivateModulesGitToken); err != nil { log.Entry().Warningf("couldn't set private packages for golang, error: %s", err.Error()) From cce7c0d384c5c3f4ca5991845cf71972f95d8cf1 Mon Sep 17 00:00:00 2001 From: Oliver Feldmann Date: Wed, 29 Nov 2023 12:29:29 +0100 Subject: [PATCH 8/8] Use new env var (#4698) --- integration/integration_tms_export_test.go | 4 ++-- integration/integration_tms_upload_test.go | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/integration/integration_tms_export_test.go b/integration/integration_tms_export_test.go index 8ee561708f..f661a89b47 100644 --- a/integration/integration_tms_export_test.go +++ b/integration/integration_tms_export_test.go @@ -19,7 +19,7 @@ func TestTmsExportIntegrationYaml(t *testing.T) { Image: "devxci/mbtci-java11-node14", User: "root", TestDir: []string{"testdata", "TestTmsIntegration"}, - Environment: map[string]string{"PIPER_tmsServiceKey": tmsServiceKey}, + Environment: map[string]string{"PIPER_serviceKey": tmsServiceKey}, }) defer container.terminate(t) @@ -41,7 +41,7 @@ func TestTmsExportIntegrationBinFailDescription(t *testing.T) { Image: "devxci/mbtci-java11-node14", User: "root", TestDir: []string{"testdata", "TestTmsIntegration"}, - Environment: map[string]string{"PIPER_tmsServiceKey": tmsServiceKey}, + Environment: map[string]string{"PIPER_serviceKey": tmsServiceKey}, }) defer container.terminate(t) diff --git a/integration/integration_tms_upload_test.go b/integration/integration_tms_upload_test.go index e7b93dc7b3..b240e2bb9c 100644 --- a/integration/integration_tms_upload_test.go +++ b/integration/integration_tms_upload_test.go @@ -32,7 +32,7 @@ func TestTmsUploadIntegrationBinSuccess(t *testing.T) { Image: "devxci/mbtci-java11-node14", User: "root", TestDir: []string{"testdata", "TestTmsIntegration"}, - Environment: map[string]string{"PIPER_tmsServiceKey": tmsServiceKey}, + Environment: map[string]string{"PIPER_serviceKey": tmsServiceKey}, }) defer container.terminate(t) @@ -58,7 +58,7 @@ func TestTmsUploadIntegrationBinNoDescriptionSuccess(t *testing.T) { Image: "devxci/mbtci-java11-node14", User: "root", TestDir: []string{"testdata", "TestTmsIntegration"}, - Environment: map[string]string{"PIPER_tmsServiceKey": tmsServiceKey}, + Environment: map[string]string{"PIPER_serviceKey": tmsServiceKey}, }) defer container.terminate(t) @@ -105,7 +105,7 @@ func TestTmsUploadIntegrationBinFailDescription(t *testing.T) { Image: "devxci/mbtci-java11-node14", User: "root", TestDir: []string{"testdata", "TestTmsIntegration"}, - Environment: map[string]string{"PIPER_tmsServiceKey": tmsServiceKey}, + Environment: map[string]string{"PIPER_serviceKey": tmsServiceKey}, }) defer container.terminate(t) @@ -126,7 +126,7 @@ func TestTmsUploadIntegrationYaml(t *testing.T) { Image: "devxci/mbtci-java11-node14", User: "root", TestDir: []string{"testdata", "TestTmsIntegration"}, - Environment: map[string]string{"PIPER_tmsServiceKey": tmsServiceKey}, + Environment: map[string]string{"PIPER_serviceKey": tmsServiceKey}, }) defer container.terminate(t)