diff --git a/cmd/golangBuild.go b/cmd/golangBuild.go index a4b0dc237a..d02cd01b28 100644 --- a/cmd/golangBuild.go +++ b/cmd/golangBuild.go @@ -2,6 +2,7 @@ package cmd import ( "bytes" + "encoding/json" "fmt" "net/http" "os" @@ -9,6 +10,7 @@ import ( "path/filepath" "strings" + "github.com/SAP/jenkins-library/pkg/build" "github.com/SAP/jenkins-library/pkg/buildsettings" "github.com/SAP/jenkins-library/pkg/certutils" "github.com/SAP/jenkins-library/pkg/command" @@ -265,7 +267,6 @@ func runGolangBuild(config *golangBuildOptions, telemetryData *telemetry.CustomD } artifactVersion, err = artifact.GetVersion() - if err != nil { return err } @@ -286,6 +287,8 @@ func runGolangBuild(config *golangBuildOptions, telemetryData *telemetry.CustomD utils.SetOptions(repoClientOptions) var binaryArtifacts piperenv.Artifacts + buildCoordinates := []versioning.Coordinates{} + for _, binary := range binaries { targetPath := fmt.Sprintf("go/%s/%s/%s", goModFile.Module.Mod.Path, artifactVersion, binary) @@ -312,14 +315,51 @@ func runGolangBuild(config *golangBuildOptions, telemetryData *telemetry.CustomD binaryArtifacts = append(binaryArtifacts, piperenv.Artifact{ Name: binary, }) + + if config.CreateBuildArtifactsMetadata { + err, coordinate := createGoBuildArtifactsMetadata(binary, config.TargetRepositoryURL, artifactVersion, utils) + if err != nil { + log.Entry().Warnf("unable to create build artifact metadata : %v", err) + } + buildCoordinates = append(buildCoordinates, coordinate) + } } commonPipelineEnvironment.custom.artifacts = binaryArtifacts + if len(buildCoordinates) == 0 { + log.Entry().Warnf("unable to identify artifact coordinates for the go binary(s) published") + return nil + } + + var buildArtifacts build.BuildArtifacts + buildArtifacts.Coordinates = buildCoordinates + jsonResult, _ := json.Marshal(buildArtifacts) + commonPipelineEnvironment.custom.goBuildArtifacts = string(jsonResult) + } return nil } +func createGoBuildArtifactsMetadata(binary string, repositoryURL string, artifactVersion string, utils golangBuildUtils) (error, versioning.Coordinates) { + options := versioning.Options{} + builtArtifact, err := versioning.GetArtifact("golang", "", &options, utils) + coordinate, err := builtArtifact.GetCoordinates() + purl := piperutils.GetPurl(filepath.Join(filepath.Dir("go.mod"), sbomFilename)) + // golang purls contain the hex code for & with GOOS and GOARC and should be reomved from the PURL + purl = strings.ReplaceAll(purl, "\\u0026", "&") + if err != nil { + return err, coordinate + } + coordinate.ArtifactID = binary + coordinate.URL = repositoryURL + coordinate.BuildPath = filepath.Dir(binary) + coordinate.PURL = purl + coordinate.Version = artifactVersion + + return nil, coordinate +} + func prepareGolangEnvironment(config *golangBuildOptions, goModFile *modfile.File, utils golangBuildUtils) error { // configure truststore err := certutils.CertificateUpdate(config.CustomTLSCertificateLinks, utils, utils, "/etc/ssl/certs/ca-certificates.crt") // TODO reimplement diff --git a/cmd/golangBuild_generated.go b/cmd/golangBuild_generated.go index 1276918a80..799d4e3414 100644 --- a/cmd/golangBuild_generated.go +++ b/cmd/golangBuild_generated.go @@ -49,11 +49,13 @@ type golangBuildOptions struct { PrivateModulesGitToken string `json:"privateModulesGitToken,omitempty"` ArtifactVersion string `json:"artifactVersion,omitempty"` GolangciLintURL string `json:"golangciLintUrl,omitempty"` + CreateBuildArtifactsMetadata bool `json:"createBuildArtifactsMetadata,omitempty"` } type golangBuildCommonPipelineEnvironment struct { custom struct { buildSettingsInfo string + goBuildArtifacts string artifacts piperenv.Artifacts } } @@ -65,6 +67,7 @@ func (p *golangBuildCommonPipelineEnvironment) persist(path, resourceName string value interface{} }{ {category: "custom", name: "buildSettingsInfo", value: p.custom.buildSettingsInfo}, + {category: "custom", name: "goBuildArtifacts", value: p.custom.goBuildArtifacts}, {category: "custom", name: "artifacts", value: p.custom.artifacts}, } @@ -286,6 +289,7 @@ func addGolangBuildFlags(cmd *cobra.Command, stepConfig *golangBuildOptions) { cmd.Flags().StringVar(&stepConfig.PrivateModulesGitToken, "privateModulesGitToken", os.Getenv("PIPER_privateModulesGitToken"), "GitHub personal access token as per https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line.") cmd.Flags().StringVar(&stepConfig.ArtifactVersion, "artifactVersion", os.Getenv("PIPER_artifactVersion"), "Version of the artifact to be built.") cmd.Flags().StringVar(&stepConfig.GolangciLintURL, "golangciLintUrl", `https://github.com/golangci/golangci-lint/releases/download/v1.51.2/golangci-lint-1.51.2-linux-amd64.tar.gz`, "Specifies the download url of the Golangci-Lint Linux amd64 tar binary file. This can be found at https://github.com/golangci/golangci-lint/releases.") + cmd.Flags().BoolVar(&stepConfig.CreateBuildArtifactsMetadata, "createBuildArtifactsMetadata", false, "metadata about the artifacts that are build and published , this metadata is generally used by steps downstream in the pipeline") cmd.MarkFlagRequired("targetArchitectures") } @@ -590,6 +594,15 @@ func golangBuildMetadata() config.StepData { Aliases: []config.Alias{}, Default: `https://github.com/golangci/golangci-lint/releases/download/v1.51.2/golangci-lint-1.51.2-linux-amd64.tar.gz`, }, + { + Name: "createBuildArtifactsMetadata", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"STEPS", "STAGES", "PARAMETERS"}, + Type: "bool", + Mandatory: false, + Aliases: []config.Alias{}, + Default: false, + }, }, }, Containers: []config.Container{ @@ -602,6 +615,7 @@ func golangBuildMetadata() config.StepData { Type: "piperEnvironment", Parameters: []map[string]interface{}{ {"name": "custom/buildSettingsInfo"}, + {"name": "custom/goBuildArtifacts"}, {"name": "custom/artifacts", "type": "piperenv.Artifacts"}, }, }, diff --git a/cmd/golangBuild_test.go b/cmd/golangBuild_test.go index 53fdbd3840..0229a3b0ac 100644 --- a/cmd/golangBuild_test.go +++ b/cmd/golangBuild_test.go @@ -205,10 +205,11 @@ go 1.17` t.Run("success - simple publish", func(t *testing.T) { config := golangBuildOptions{ - TargetArchitectures: []string{"linux,amd64"}, - Publish: true, - TargetRepositoryURL: "https://my.target.repository.local/", - ArtifactVersion: "1.0.0", + TargetArchitectures: []string{"linux,amd64"}, + Publish: true, + TargetRepositoryURL: "https://my.target.repository.local/", + ArtifactVersion: "1.0.0", + CreateBuildArtifactsMetadata: false, } utils := newGolangBuildTestsUtils() @@ -223,14 +224,16 @@ go 1.17` t.Run("success - publishes binaries", func(t *testing.T) { config := golangBuildOptions{ - TargetArchitectures: []string{"linux,amd64"}, - Output: "testBin", - Publish: true, - TargetRepositoryURL: "https://my.target.repository.local", - TargetRepositoryUser: "user", - TargetRepositoryPassword: "password", - ArtifactVersion: "1.0.0", + TargetArchitectures: []string{"linux,amd64"}, + Output: "testBin", + Publish: true, + CreateBuildArtifactsMetadata: false, + TargetRepositoryURL: "https://my.target.repository.local", + TargetRepositoryUser: "user", + TargetRepositoryPassword: "password", + ArtifactVersion: "1.0.0", } + utils := newGolangBuildTestsUtils() utils.returnFileUploadStatus = 201 utils.FilesMock.AddFile("go.mod", []byte("module example.com/my/module")) @@ -248,13 +251,14 @@ go 1.17` t.Run("success - publishes binaries (when TargetRepositoryURL ends with slash)", func(t *testing.T) { config := golangBuildOptions{ - TargetArchitectures: []string{"linux,amd64"}, - Output: "testBin", - Publish: true, - TargetRepositoryURL: "https://my.target.repository.local/", - TargetRepositoryUser: "user", - TargetRepositoryPassword: "password", - ArtifactVersion: "1.0.0", + TargetArchitectures: []string{"linux,amd64"}, + Output: "testBin", + Publish: true, + CreateBuildArtifactsMetadata: false, + TargetRepositoryURL: "https://my.target.repository.local/", + TargetRepositoryUser: "user", + TargetRepositoryPassword: "password", + ArtifactVersion: "1.0.0", } utils := newGolangBuildTestsUtils() utils.returnFileUploadStatus = 200 @@ -1038,6 +1042,38 @@ func TestRunGolangciLint(t *testing.T) { } } +func TestGoCreateBuildArtifactMetadata(t *testing.T) { + config := golangBuildOptions{ + TargetArchitectures: []string{"linux,amd64"}, + Output: "testBin", + Publish: true, + TargetRepositoryURL: "https://my.target.repository.local", + TargetRepositoryUser: "user", + TargetRepositoryPassword: "password", + } + utils := newGolangBuildTestsUtils() + versionFile, err := os.Create("VERSION") + + assert.Equal(t, err, nil) + // Ensure file is closed and deleted after function finishes + defer versionFile.Close() + defer os.Remove("VERSION") // Delete the file when the function exits + + // Write something to the file + _, err = versionFile.WriteString("1.0.0") + + // Create a new file + binaryFile, err := os.Create("testBin") + assert.Equal(t, err, nil) + + defer binaryFile.Close() + defer os.Remove("testBin") // Delete the file when the function exits + + err, version := createGoBuildArtifactsMetadata("testBin", config.TargetRepositoryURL, "1.0.0", utils) + assert.Equal(t, err, nil) + assert.Equal(t, version.ArtifactID, "testBin") +} + func TestRetrieveGolangciLint(t *testing.T) { t.Parallel() diff --git a/resources/metadata/golangBuild.yaml b/resources/metadata/golangBuild.yaml index 6478367ebd..669ac0f848 100644 --- a/resources/metadata/golangBuild.yaml +++ b/resources/metadata/golangBuild.yaml @@ -257,12 +257,21 @@ spec: - PARAMETERS - STEPS default: "https://github.com/golangci/golangci-lint/releases/download/v1.51.2/golangci-lint-1.51.2-linux-amd64.tar.gz" + - name: createBuildArtifactsMetadata + type: bool + default: false + description: metadata about the artifacts that are build and published , this metadata is generally used by steps downstream in the pipeline + scope: + - STEPS + - STAGES + - PARAMETERS outputs: resources: - name: commonPipelineEnvironment type: piperEnvironment params: - name: custom/buildSettingsInfo + - name: custom/goBuildArtifacts - name: custom/artifacts type: "piperenv.Artifacts" - name: reports