mirror of
https://github.com/SAP/jenkins-library.git
synced 2024-12-12 10:55:20 +02:00
414 lines
13 KiB
Go
414 lines
13 KiB
Go
package cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"regexp"
|
|
"strings"
|
|
"text/template"
|
|
|
|
"github.com/SAP/jenkins-library/pkg/certutils"
|
|
"github.com/SAP/jenkins-library/pkg/command"
|
|
"github.com/SAP/jenkins-library/pkg/goget"
|
|
piperhttp "github.com/SAP/jenkins-library/pkg/http"
|
|
"github.com/SAP/jenkins-library/pkg/log"
|
|
"github.com/SAP/jenkins-library/pkg/piperenv"
|
|
"github.com/SAP/jenkins-library/pkg/piperutils"
|
|
"github.com/SAP/jenkins-library/pkg/telemetry"
|
|
|
|
"golang.org/x/mod/modfile"
|
|
"golang.org/x/mod/module"
|
|
)
|
|
|
|
const (
|
|
coverageFile = "cover.out"
|
|
golangUnitTestOutput = "TEST-go.xml"
|
|
golangIntegrationTestOutput = "TEST-integration.xml"
|
|
golangCoberturaPackage = "github.com/boumenot/gocover-cobertura@latest"
|
|
golangTestsumPackage = "gotest.tools/gotestsum@latest"
|
|
)
|
|
|
|
type golangBuildUtils interface {
|
|
command.ExecRunner
|
|
goget.Client
|
|
|
|
piperutils.FileUtils
|
|
piperhttp.Uploader
|
|
|
|
// Add more methods here, or embed additional interfaces, or remove/replace as required.
|
|
// The golangBuildUtils interface should be descriptive of your runtime dependencies,
|
|
// i.e. include everything you need to be able to mock in tests.
|
|
// Unit tests shall be executable in parallel (not depend on global state), and don't (re-)test dependencies.
|
|
}
|
|
|
|
type golangBuildUtilsBundle struct {
|
|
*command.Command
|
|
*piperutils.Files
|
|
piperhttp.Uploader
|
|
goget.Client
|
|
|
|
// Embed more structs as necessary to implement methods or interfaces you add to golangBuildUtils.
|
|
// Structs embedded in this way must each have a unique set of methods attached.
|
|
// If there is no struct which implements the method you need, attach the method to
|
|
// golangBuildUtilsBundle and forward to the implementation of the dependency.
|
|
}
|
|
|
|
func newGolangBuildUtils(config golangBuildOptions) golangBuildUtils {
|
|
httpClientOptions := piperhttp.ClientOptions{}
|
|
|
|
if len(config.CustomTLSCertificateLinks) > 0 {
|
|
httpClientOptions.TransportSkipVerification = false
|
|
httpClientOptions.TrustedCerts = config.CustomTLSCertificateLinks
|
|
}
|
|
|
|
httpClient := piperhttp.Client{}
|
|
httpClient.SetOptions(httpClientOptions)
|
|
|
|
utils := golangBuildUtilsBundle{
|
|
Command: &command.Command{},
|
|
Files: &piperutils.Files{},
|
|
Uploader: &httpClient,
|
|
Client: &goget.ClientImpl{
|
|
HTTPClient: &httpClient,
|
|
},
|
|
}
|
|
// Reroute command output to logging framework
|
|
utils.Stdout(log.Writer())
|
|
utils.Stderr(log.Writer())
|
|
return &utils
|
|
}
|
|
|
|
func golangBuild(config golangBuildOptions, telemetryData *telemetry.CustomData) {
|
|
// Utils can be used wherever the command.ExecRunner interface is expected.
|
|
// It can also be used for example as a mavenExecRunner.
|
|
utils := newGolangBuildUtils(config)
|
|
|
|
// Error situations will be bubbled up until they reach the line below which will then stop execution
|
|
// through the log.Entry().Fatal() call leading to an os.Exit(1) in the end.
|
|
err := runGolangBuild(&config, telemetryData, utils)
|
|
if err != nil {
|
|
log.Entry().WithError(err).Fatal("execution of golang build failed")
|
|
}
|
|
}
|
|
|
|
func runGolangBuild(config *golangBuildOptions, telemetryData *telemetry.CustomData, utils golangBuildUtils) error {
|
|
err := prepareGolangEnvironment(config, utils)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// install test pre-requisites only in case testing should be performed
|
|
if config.RunTests || config.RunIntegrationTests {
|
|
if err := utils.RunExecutable("go", "install", golangTestsumPackage); err != nil {
|
|
return fmt.Errorf("failed to install pre-requisite: %w", err)
|
|
}
|
|
}
|
|
|
|
failedTests := false
|
|
|
|
if config.RunTests {
|
|
success, err := runGolangTests(config, utils)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
failedTests = !success
|
|
}
|
|
|
|
if config.RunTests && config.ReportCoverage {
|
|
if err := reportGolangTestCoverage(config, utils); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if config.RunIntegrationTests {
|
|
success, err := runGolangIntegrationTests(config, utils)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
failedTests = failedTests || !success
|
|
}
|
|
|
|
if failedTests {
|
|
log.SetErrorCategory(log.ErrorTest)
|
|
return fmt.Errorf("some tests failed")
|
|
}
|
|
|
|
ldflags := ""
|
|
|
|
if len(config.LdflagsTemplate) > 0 {
|
|
var err error
|
|
ldflags, err = prepareLdflags(config, utils, GeneralConfig.EnvRootPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log.Entry().Infof("ldflags from template: '%v'", ldflags)
|
|
}
|
|
|
|
binaries := []string{}
|
|
|
|
for _, architecture := range config.TargetArchitectures {
|
|
binary, err := runGolangBuildPerArchitecture(config, utils, ldflags, architecture)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(binary) > 0 {
|
|
binaries = append(binaries, binary)
|
|
}
|
|
}
|
|
|
|
if config.Publish {
|
|
if len(config.TargetRepositoryURL) == 0 {
|
|
return fmt.Errorf("there's no target repository for binary publishing configured")
|
|
}
|
|
|
|
repoClientOptions := piperhttp.ClientOptions{
|
|
Username: config.TargetRepositoryUser,
|
|
Password: config.TargetRepositoryPassword,
|
|
TrustedCerts: config.CustomTLSCertificateLinks,
|
|
}
|
|
|
|
utils.SetOptions(repoClientOptions)
|
|
|
|
for _, binary := range binaries {
|
|
log.Entry().Infof("publishing artifact '%s'", binary)
|
|
|
|
response, err := utils.UploadFile(fmt.Sprintf("%s/%s", config.TargetRepositoryURL, binary), binary, "", nil, nil, "binary")
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't upload artifact: %w", err)
|
|
}
|
|
|
|
if response.StatusCode != 200 {
|
|
return fmt.Errorf("couldn't upload artifact, received status code %d", response.StatusCode)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func prepareGolangEnvironment(config *golangBuildOptions, utils golangBuildUtils) error {
|
|
// configure truststore
|
|
err := certutils.CertificateUpdate(config.CustomTLSCertificateLinks, utils, utils, "/etc/ssl/certs/ca-certificates.crt") // TODO reimplement
|
|
|
|
if config.PrivateModules == "" {
|
|
return nil
|
|
}
|
|
|
|
if config.PrivateModulesGitToken == "" {
|
|
return fmt.Errorf("please specify a token for fetching private git modules")
|
|
}
|
|
|
|
// pass private repos to go process
|
|
os.Setenv("GOPRIVATE", config.PrivateModules)
|
|
|
|
repoURLs, err := lookupGolangPrivateModulesRepositories(config.PrivateModules, utils)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// configure credentials git shall use for pulling repos
|
|
for _, repoURL := range repoURLs {
|
|
if match, _ := regexp.MatchString("(?i)^https?://", repoURL); !match {
|
|
continue
|
|
}
|
|
|
|
authenticatedRepoURL := strings.Replace(repoURL, "://", fmt.Sprintf("://%s@", config.PrivateModulesGitToken), 1)
|
|
|
|
err = utils.RunExecutable("git", "config", "--global", fmt.Sprintf("url.%s.insteadOf", authenticatedRepoURL), fmt.Sprintf("%s", repoURL))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func runGolangTests(config *golangBuildOptions, utils golangBuildUtils) (bool, error) {
|
|
// execute gotestsum in order to have more output options
|
|
if err := utils.RunExecutable("gotestsum", "--junitfile", golangUnitTestOutput, "--", fmt.Sprintf("-coverprofile=%v", coverageFile), "./..."); err != nil {
|
|
exists, fileErr := utils.FileExists(golangUnitTestOutput)
|
|
if !exists || fileErr != nil {
|
|
log.SetErrorCategory(log.ErrorBuild)
|
|
return false, fmt.Errorf("running tests failed - junit result missing: %w", err)
|
|
}
|
|
exists, fileErr = utils.FileExists(coverageFile)
|
|
if !exists || fileErr != nil {
|
|
log.SetErrorCategory(log.ErrorBuild)
|
|
return false, fmt.Errorf("running tests failed - coverage output missing: %w", err)
|
|
}
|
|
return false, nil
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func runGolangIntegrationTests(config *golangBuildOptions, utils golangBuildUtils) (bool, error) {
|
|
// execute gotestsum in order to have more output options
|
|
// for integration tests coverage data is not meaningful and thus not being created
|
|
if err := utils.RunExecutable("gotestsum", "--junitfile", golangIntegrationTestOutput, "--", "-tags=integration", "./..."); err != nil {
|
|
exists, fileErr := utils.FileExists(golangIntegrationTestOutput)
|
|
if !exists || fileErr != nil {
|
|
log.SetErrorCategory(log.ErrorBuild)
|
|
return false, fmt.Errorf("running tests failed: %w", err)
|
|
}
|
|
return false, nil
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func reportGolangTestCoverage(config *golangBuildOptions, utils golangBuildUtils) error {
|
|
if config.CoverageFormat == "cobertura" {
|
|
// execute gocover-cobertura in order to create cobertura report
|
|
// install pre-requisites
|
|
if err := utils.RunExecutable("go", "install", golangCoberturaPackage); err != nil {
|
|
return fmt.Errorf("failed to install pre-requisite: %w", err)
|
|
}
|
|
|
|
coverageData, err := utils.FileRead(coverageFile)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read coverage file %v: %w", coverageFile, err)
|
|
}
|
|
utils.Stdin(bytes.NewBuffer(coverageData))
|
|
|
|
coverageOutput := bytes.Buffer{}
|
|
utils.Stdout(&coverageOutput)
|
|
options := []string{}
|
|
if config.ExcludeGeneratedFromCoverage {
|
|
options = append(options, "-ignore-gen-files")
|
|
}
|
|
if err := utils.RunExecutable("gocover-cobertura", options...); err != nil {
|
|
log.SetErrorCategory(log.ErrorTest)
|
|
return fmt.Errorf("failed to convert coverage data to cobertura format: %w", err)
|
|
}
|
|
utils.Stdout(log.Writer())
|
|
|
|
err = utils.FileWrite("cobertura-coverage.xml", coverageOutput.Bytes(), 0666)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create cobertura coverage file: %w", err)
|
|
}
|
|
log.Entry().Info("created file cobertura-coverage.xml")
|
|
} else {
|
|
// currently only cobertura and html format supported, thus using html as fallback
|
|
if err := utils.RunExecutable("go", "tool", "cover", "-html", coverageFile, "-o", "coverage.html"); err != nil {
|
|
return fmt.Errorf("failed to create html coverage file: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func prepareLdflags(config *golangBuildOptions, utils golangBuildUtils, envRootPath string) (string, error) {
|
|
cpe := piperenv.CPEMap{}
|
|
err := cpe.LoadFromDisk(path.Join(envRootPath, "commonPipelineEnvironment"))
|
|
if err != nil {
|
|
log.Entry().Warning("failed to load values from commonPipelineEnvironment")
|
|
}
|
|
|
|
log.Entry().Debugf("ldflagsTemplate in use: %v", config.LdflagsTemplate)
|
|
tmpl, err := template.New("ldflags").Parse(config.LdflagsTemplate)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to parse ldflagsTemplate '%v': %w", config.LdflagsTemplate, err)
|
|
}
|
|
|
|
ldflagsParams := struct {
|
|
CPE map[string]interface{}
|
|
}{
|
|
CPE: map[string]interface{}(cpe),
|
|
}
|
|
var generatedLdflags bytes.Buffer
|
|
err = tmpl.Execute(&generatedLdflags, ldflagsParams)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to execute ldflagsTemplate '%v': %w", config.LdflagsTemplate, err)
|
|
}
|
|
|
|
return generatedLdflags.String(), nil
|
|
}
|
|
|
|
func runGolangBuildPerArchitecture(config *golangBuildOptions, utils golangBuildUtils, ldflags, architecture string) (string, error) {
|
|
var binaryName string
|
|
|
|
envVars := os.Environ()
|
|
goos, goarch := splitTargetArchitecture(architecture)
|
|
envVars = append(envVars, fmt.Sprintf("GOOS=%v", goos), fmt.Sprintf("GOARCH=%v", goarch))
|
|
|
|
if !config.CgoEnabled {
|
|
envVars = append(envVars, "CGO_ENABLED=0")
|
|
}
|
|
utils.SetEnv(envVars)
|
|
|
|
buildOptions := []string{"build"}
|
|
if len(config.Output) > 0 {
|
|
fileExtension := ""
|
|
if goos == "windows" {
|
|
fileExtension = ".exe"
|
|
}
|
|
binaryName = fmt.Sprintf("%v-%v.%v%v", config.Output, goos, goarch, fileExtension)
|
|
buildOptions = append(buildOptions, "-o", binaryName)
|
|
}
|
|
buildOptions = append(buildOptions, config.BuildFlags...)
|
|
buildOptions = append(buildOptions, config.Packages...)
|
|
if len(ldflags) > 0 {
|
|
buildOptions = append(buildOptions, "-ldflags", ldflags)
|
|
}
|
|
|
|
if err := utils.RunExecutable("go", buildOptions...); err != nil {
|
|
log.Entry().Debugf("buildOptions: %v", buildOptions)
|
|
log.SetErrorCategory(log.ErrorBuild)
|
|
return "", fmt.Errorf("failed to run build for %v.%v: %w", goos, goarch, err)
|
|
}
|
|
return binaryName, nil
|
|
}
|
|
|
|
func splitTargetArchitecture(architecture string) (string, string) {
|
|
// architecture expected to be in format os,arch due to possibleValues check of step
|
|
|
|
architectureParts := strings.Split(architecture, ",")
|
|
return architectureParts[0], architectureParts[1]
|
|
}
|
|
|
|
// lookupPrivateModulesRepositories returns a slice of all modules that match the given glob pattern
|
|
func lookupGolangPrivateModulesRepositories(globPattern string, utils golangBuildUtils) ([]string, error) {
|
|
if globPattern == "" {
|
|
return []string{}, nil
|
|
}
|
|
|
|
if modFileExists, err := utils.FileExists("go.mod"); err != nil {
|
|
return nil, err
|
|
} else if !modFileExists {
|
|
return []string{}, nil // nothing to do
|
|
}
|
|
|
|
modFileContent, err := utils.FileRead("go.mod")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
goModFile, err := modfile.Parse("go.mod", modFileContent, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
} else if goModFile.Require == nil {
|
|
return []string{}, nil // no modules referenced, nothing to do
|
|
}
|
|
|
|
privateModules := []string{}
|
|
|
|
for _, goModule := range goModFile.Require {
|
|
if !module.MatchPrefixPatterns(globPattern, goModule.Mod.Path) {
|
|
continue
|
|
}
|
|
|
|
repo, err := utils.GetRepositoryURL(goModule.Mod.Path)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
privateModules = append(privateModules, repo)
|
|
}
|
|
return privateModules, nil
|
|
}
|