You've already forked sap-jenkins-library
mirror of
https://github.com/SAP/jenkins-library.git
synced 2025-07-15 01:34:38 +02:00
feat(golangBuild): support private modules (#3471)
* feat(golangBuild): support private module repositories
This commit is contained in:
@ -5,14 +5,21 @@ import (
|
||||
"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 (
|
||||
@ -25,10 +32,10 @@ const (
|
||||
|
||||
type golangBuildUtils interface {
|
||||
command.ExecRunner
|
||||
goget.Client
|
||||
|
||||
FileExists(filename string) (bool, error)
|
||||
FileRead(path string) ([]byte, error)
|
||||
FileWrite(path string, content []byte, perm os.FileMode) error
|
||||
piperutils.FileUtils
|
||||
piperhttp.Sender
|
||||
|
||||
// Add more methods here, or embed additional interfaces, or remove/replace as required.
|
||||
// The golangBuildUtils interface should be descriptive of your runtime dependencies,
|
||||
@ -39,6 +46,8 @@ type golangBuildUtils interface {
|
||||
type golangBuildUtilsBundle struct {
|
||||
*command.Command
|
||||
*piperutils.Files
|
||||
piperhttp.Sender
|
||||
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.
|
||||
@ -46,10 +55,24 @@ type golangBuildUtilsBundle struct {
|
||||
// golangBuildUtilsBundle and forward to the implementation of the dependency.
|
||||
}
|
||||
|
||||
func newGolangBuildUtils() golangBuildUtils {
|
||||
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{},
|
||||
Sender: &httpClient,
|
||||
Client: &goget.ClientImpl{
|
||||
HTTPClient: &httpClient,
|
||||
},
|
||||
}
|
||||
// Reroute command output to logging framework
|
||||
utils.Stdout(log.Writer())
|
||||
@ -60,7 +83,7 @@ func newGolangBuildUtils() golangBuildUtils {
|
||||
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()
|
||||
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.
|
||||
@ -71,6 +94,10 @@ func golangBuild(config golangBuildOptions, telemetryData *telemetry.CustomData)
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -129,6 +156,44 @@ func runGolangBuild(config *golangBuildOptions, telemetryData *telemetry.CustomD
|
||||
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 {
|
||||
@ -266,3 +331,45 @@ func splitTargetArchitecture(architecture string) (string, string) {
|
||||
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
|
||||
}
|
||||
|
@ -32,6 +32,8 @@ type golangBuildOptions struct {
|
||||
TargetArchitectures []string `json:"targetArchitectures,omitempty"`
|
||||
TestOptions []string `json:"testOptions,omitempty"`
|
||||
TestResultFormat string `json:"testResultFormat,omitempty" validate:"possible-values=junit standard"`
|
||||
PrivateModules string `json:"privateModules,omitempty"`
|
||||
PrivateModulesGitToken string `json:"privateModulesGitToken,omitempty"`
|
||||
}
|
||||
|
||||
// GolangBuildCommand This step will execute a golang build.
|
||||
@ -70,6 +72,7 @@ If the build is successful the resulting artifact can be uploaded to e.g. a bina
|
||||
log.SetErrorCategory(log.ErrorConfiguration)
|
||||
return err
|
||||
}
|
||||
log.RegisterSecret(stepConfig.PrivateModulesGitToken)
|
||||
|
||||
if len(GeneralConfig.HookConfig.SentryConfig.Dsn) > 0 {
|
||||
sentryHook := log.NewSentryHook(GeneralConfig.HookConfig.SentryConfig.Dsn, GeneralConfig.CorrelationID)
|
||||
@ -144,6 +147,8 @@ func addGolangBuildFlags(cmd *cobra.Command, stepConfig *golangBuildOptions) {
|
||||
cmd.Flags().StringSliceVar(&stepConfig.TargetArchitectures, "targetArchitectures", []string{`linux,amd64`}, "Defines the target architectures for which the build should run using OS and architecture separated by a comma.")
|
||||
cmd.Flags().StringSliceVar(&stepConfig.TestOptions, "testOptions", []string{}, "Options to pass to test as per `go test` documentation (comprises e.g. flags, packages).")
|
||||
cmd.Flags().StringVar(&stepConfig.TestResultFormat, "testResultFormat", `junit`, "Defines the output format of the test results.")
|
||||
cmd.Flags().StringVar(&stepConfig.PrivateModules, "privateModules", os.Getenv("PIPER_privateModules"), "Tells go which modules shall be considered to be private (by setting [GOPRIVATE](https://pkg.go.dev/cmd/go#hdr-Configuration_for_downloading_non_public_code)).")
|
||||
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.MarkFlagRequired("targetArchitectures")
|
||||
}
|
||||
@ -158,6 +163,9 @@ func golangBuildMetadata() config.StepData {
|
||||
},
|
||||
Spec: config.StepSpec{
|
||||
Inputs: config.StepInputs{
|
||||
Secrets: []config.StepSecrets{
|
||||
{Name: "golangPrivateModulesGitTokenCredentialsId", Description: "Jenkins 'Username with password' credentials ID containing username/password for http access to your git repos where your go private modules are stored.", Type: "jenkins"},
|
||||
},
|
||||
Parameters: []config.StepParameters{
|
||||
{
|
||||
Name: "buildFlags",
|
||||
@ -303,6 +311,35 @@ func golangBuildMetadata() config.StepData {
|
||||
Aliases: []config.Alias{},
|
||||
Default: `junit`,
|
||||
},
|
||||
{
|
||||
Name: "privateModules",
|
||||
ResourceRef: []config.ResourceReference{},
|
||||
Scope: []string{"STEPS", "STAGES", "PARAMETERS"},
|
||||
Type: "string",
|
||||
Mandatory: false,
|
||||
Aliases: []config.Alias{},
|
||||
Default: os.Getenv("PIPER_privateModules"),
|
||||
},
|
||||
{
|
||||
Name: "privateModulesGitToken",
|
||||
ResourceRef: []config.ResourceReference{
|
||||
{
|
||||
Name: "golangPrivateModulesGitTokenCredentialsId",
|
||||
Type: "secret",
|
||||
},
|
||||
|
||||
{
|
||||
Name: "golangPrivateModulesGitTokenVaultSecret",
|
||||
Type: "vaultSecret",
|
||||
Default: "golang",
|
||||
},
|
||||
},
|
||||
Scope: []string{"GENERAL", "PARAMETERS", "STAGES", "STEPS"},
|
||||
Type: "string",
|
||||
Mandatory: false,
|
||||
Aliases: []config.Alias{},
|
||||
Default: os.Getenv("PIPER_privateModulesGitToken"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Containers: []config.Container{
|
||||
|
@ -2,11 +2,14 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
piperhttp "github.com/SAP/jenkins-library/pkg/http"
|
||||
"github.com/SAP/jenkins-library/pkg/mock"
|
||||
"github.com/SAP/jenkins-library/pkg/telemetry"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -17,6 +20,18 @@ type golangBuildMockUtils struct {
|
||||
*mock.FilesMock
|
||||
}
|
||||
|
||||
func (utils golangBuildMockUtils) GetRepositoryURL(module string) (string, error) {
|
||||
return fmt.Sprintf("https://%s.git", module), nil
|
||||
}
|
||||
|
||||
func (utils golangBuildMockUtils) SendRequest(method string, url string, r io.Reader, header http.Header, cookies []*http.Cookie) (*http.Response, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
func (utils golangBuildMockUtils) SetOptions(options piperhttp.ClientOptions) {
|
||||
// not implemented
|
||||
}
|
||||
|
||||
func newGolangBuildTestsUtils() golangBuildMockUtils {
|
||||
utils := golangBuildMockUtils{
|
||||
ExecMockRunner: &mock.ExecMockRunner{},
|
||||
@ -432,3 +447,157 @@ func TestRunGolangBuildPerArchitecture(t *testing.T) {
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestPrepareGolangEnvironment(t *testing.T) {
|
||||
modTestFile := `
|
||||
module private.example.com/m
|
||||
|
||||
require (
|
||||
example.com/public/module v1.0.0
|
||||
private1.example.com/private/repo v0.1.0
|
||||
private2.example.com/another/repo v0.2.0
|
||||
)
|
||||
|
||||
go 1.17`
|
||||
|
||||
type expectations struct {
|
||||
envVars []string
|
||||
commandsExecuted [][]string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
modFileContent string
|
||||
globPattern string
|
||||
gitToken string
|
||||
expect expectations
|
||||
}{
|
||||
{
|
||||
name: "success - does nothing if privateModules is not set",
|
||||
modFileContent: modTestFile,
|
||||
globPattern: "",
|
||||
gitToken: "secret",
|
||||
expect: expectations{},
|
||||
},
|
||||
{
|
||||
name: "success - goprivate is set and authentication properly configured",
|
||||
modFileContent: modTestFile,
|
||||
globPattern: "*.example.com",
|
||||
gitToken: "secret",
|
||||
expect: expectations{
|
||||
envVars: []string{"GOPRIVATE=*.example.com"},
|
||||
commandsExecuted: [][]string{
|
||||
[]string{"git", "config", "--global", "url.https://secret@private1.example.com/private/repo.git.insteadOf", "https://private1.example.com/private/repo.git"},
|
||||
[]string{"git", "config", "--global", "url.https://secret@private2.example.com/another/repo.git.insteadOf", "https://private2.example.com/another/repo.git"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
utils := newGolangBuildTestsUtils()
|
||||
utils.FilesMock.AddFile("go.mod", []byte(tt.modFileContent))
|
||||
|
||||
config := golangBuildOptions{}
|
||||
config.PrivateModules = tt.globPattern
|
||||
config.PrivateModulesGitToken = tt.gitToken
|
||||
|
||||
err := prepareGolangEnvironment(&config, &utils)
|
||||
|
||||
if assert.NoError(t, err) {
|
||||
assert.Subset(t, os.Environ(), tt.expect.envVars)
|
||||
assert.Equal(t, len(tt.expect.commandsExecuted), len(utils.Calls))
|
||||
|
||||
for i, expectedCommand := range tt.expect.commandsExecuted {
|
||||
assert.Equal(t, expectedCommand[0], utils.Calls[i].Exec)
|
||||
assert.Equal(t, expectedCommand[1:], utils.Calls[i].Params)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLookupGolangPrivateModulesRepositories(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
modTestFile := `
|
||||
module private.example.com/m
|
||||
|
||||
require (
|
||||
example.com/public/module v1.0.0
|
||||
private1.example.com/private/repo v0.1.0
|
||||
private2.example.com/another/repo v0.2.0
|
||||
)
|
||||
|
||||
go 1.17`
|
||||
|
||||
type expectations struct {
|
||||
repos []string
|
||||
errorMessage string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
modFileContent string
|
||||
globPattern string
|
||||
expect expectations
|
||||
}{
|
||||
{
|
||||
name: "Does nothing if glob pattern is empty",
|
||||
modFileContent: modTestFile,
|
||||
expect: expectations{
|
||||
repos: []string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Does nothing if there is no go.mod file",
|
||||
globPattern: "private.example.com",
|
||||
modFileContent: "",
|
||||
expect: expectations{
|
||||
repos: []string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Detects all private repos using a glob pattern",
|
||||
modFileContent: modTestFile,
|
||||
globPattern: "*.example.com",
|
||||
expect: expectations{
|
||||
repos: []string{"https://private1.example.com/private/repo.git", "https://private2.example.com/another/repo.git"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Detects all private repos",
|
||||
modFileContent: modTestFile,
|
||||
globPattern: "private1.example.com,private2.example.com",
|
||||
expect: expectations{
|
||||
repos: []string{"https://private1.example.com/private/repo.git", "https://private2.example.com/another/repo.git"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Detects a dedicated repo",
|
||||
modFileContent: modTestFile,
|
||||
globPattern: "private2.example.com",
|
||||
expect: expectations{
|
||||
repos: []string{"https://private2.example.com/another/repo.git"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
utils := newGolangBuildTestsUtils()
|
||||
|
||||
if tt.modFileContent != "" {
|
||||
utils.FilesMock.AddFile("go.mod", []byte(tt.modFileContent))
|
||||
}
|
||||
|
||||
repos, err := lookupGolangPrivateModulesRepositories(tt.globPattern, utils)
|
||||
|
||||
if tt.expect.errorMessage == "" {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expect.repos, repos)
|
||||
} else {
|
||||
assert.EqualError(t, err, tt.expect.errorMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
1
go.mod
1
go.mod
@ -7,6 +7,7 @@ require (
|
||||
github.com/GoogleContainerTools/container-diff v0.17.0
|
||||
github.com/Jeffail/gabs/v2 v2.6.1
|
||||
github.com/Masterminds/sprig v2.22.0+incompatible
|
||||
github.com/antchfx/htmlquery v1.2.4
|
||||
github.com/bmatcuk/doublestar v1.3.4
|
||||
github.com/bndr/gojenkins v1.1.1-0.20210520222939-90ed82bfdff6
|
||||
github.com/buildpacks/lifecycle v0.13.0
|
||||
|
5
go.sum
5
go.sum
@ -210,6 +210,10 @@ github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190307165228-86c17b95fcd5/go.mod h1
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/antchfx/htmlquery v1.2.4 h1:qLteofCMe/KGovBI6SQgmou2QNyedFUW+pE+BpeZ494=
|
||||
github.com/antchfx/htmlquery v1.2.4/go.mod h1:2xO6iu3EVWs7R2JYqBbp8YzG50gj/ofqs5/0VZoDZLc=
|
||||
github.com/antchfx/xpath v1.2.0 h1:mbwv7co+x0RwgeGAOHdrKy89GvHaGvxxBtPK0uF9Zr8=
|
||||
github.com/antchfx/xpath v1.2.0/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/apache/arrow/go/arrow v0.0.0-20200601151325-b2287a20f230 h1:5ultmol0yeX75oh1hY78uAFn3dupBQ/QUNxERCkiaUQ=
|
||||
github.com/apache/arrow/go/arrow v0.0.0-20200601151325-b2287a20f230/go.mod h1:QNYViu/X0HXDHw7m3KXzWSVXIbfUvJqBFe6Gj8/pYA0=
|
||||
@ -2058,6 +2062,7 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200505041828-1ed23360d12c/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
|
@ -9,7 +9,14 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// CertificateUpdate adds certificates to the given truststore
|
||||
func CertificateUpdate(certLinks []string, httpClient piperhttp.Sender, fileUtils piperutils.FileUtils, caCertsFile string) error {
|
||||
// TODO this implementation doesn't work on non-linux machines, is not failsafe and should be implemented differently
|
||||
|
||||
if len(certLinks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
caCerts, err := fileUtils.FileRead(caCertsFile)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to load file '%v'", caCertsFile)
|
||||
|
54
pkg/goget/goget.go
Normal file
54
pkg/goget/goget.go
Normal file
@ -0,0 +1,54 @@
|
||||
package goget
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
piperhttp "github.com/SAP/jenkins-library/pkg/http"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/antchfx/htmlquery"
|
||||
)
|
||||
|
||||
// Client .
|
||||
type Client interface {
|
||||
GetRepositoryURL(module string) (string, error)
|
||||
}
|
||||
|
||||
// ClientImpl .
|
||||
type ClientImpl struct {
|
||||
HTTPClient piperhttp.Sender
|
||||
}
|
||||
|
||||
// GetRepositoryURL resolves the repository URL for the given go module. Only git is supported.
|
||||
func (c *ClientImpl) GetRepositoryURL(module string) (string, error) {
|
||||
response, err := c.HTTPClient.SendRequest("GET", fmt.Sprintf("https://%s?go-get=1", module), nil, http.Header{}, nil)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else if response.StatusCode == 404 {
|
||||
return "", fmt.Errorf("module '%s' doesn't exist", module)
|
||||
} else if response.StatusCode != 200 {
|
||||
return "", fmt.Errorf("received unexpected response status code: %d", response.StatusCode)
|
||||
}
|
||||
|
||||
html, err := htmlquery.Parse(response.Body)
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to parse content: %q", err)
|
||||
}
|
||||
|
||||
metaNode := htmlquery.FindOne(html, "//meta[@name='go-import']/@content")
|
||||
|
||||
if metaNode == nil {
|
||||
return "", fmt.Errorf("couldn't find go-import statement")
|
||||
}
|
||||
|
||||
goImportStatement := htmlquery.SelectAttr(metaNode, "content")
|
||||
goImport := strings.Split(goImportStatement, " ")
|
||||
|
||||
if len(goImport) != 3 || goImport[1] != "git" {
|
||||
return "", fmt.Errorf("unsupported module: '%s'", module)
|
||||
}
|
||||
|
||||
return goImport[2], nil
|
||||
}
|
144
pkg/goget/goget_test.go
Normal file
144
pkg/goget/goget_test.go
Normal file
@ -0,0 +1,144 @@
|
||||
package goget
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
piperhttp "github.com/SAP/jenkins-library/pkg/http"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGoGetClient(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type expectations struct {
|
||||
registryURL string
|
||||
errorMessage string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
goModuleURL string
|
||||
vcs string
|
||||
registryURL string
|
||||
returnStatus int
|
||||
returnContent string
|
||||
expect expectations
|
||||
}{
|
||||
{
|
||||
name: "success case",
|
||||
goModuleURL: "example.com/my/repo",
|
||||
vcs: "git",
|
||||
registryURL: "https://git.example.com/my/repo.git",
|
||||
returnStatus: 200,
|
||||
expect: expectations{
|
||||
registryURL: "https://git.example.com/my/repo.git",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error - module doesn't exist",
|
||||
goModuleURL: "example.com/my/repo",
|
||||
vcs: "git",
|
||||
returnStatus: 404,
|
||||
expect: expectations{errorMessage: "module 'example.com/my/repo' doesn't exist"},
|
||||
},
|
||||
{
|
||||
name: "error - unexpected status code",
|
||||
goModuleURL: "example.com/my/repo",
|
||||
vcs: "git",
|
||||
returnStatus: 401,
|
||||
expect: expectations{errorMessage: "received unexpected response status code: 401"},
|
||||
},
|
||||
{
|
||||
name: "error - endpoint doesn't implement the go-import protocol",
|
||||
returnStatus: 200,
|
||||
returnContent: "<!DOCTYPE html>\n<html lang=\"en\"><head></head></html>",
|
||||
expect: expectations{errorMessage: "couldn't find go-import statement"},
|
||||
},
|
||||
{
|
||||
name: "error - unsupported vcs",
|
||||
returnStatus: 200,
|
||||
goModuleURL: "example.com/my/repo",
|
||||
vcs: "svn",
|
||||
registryURL: "https://svn.example.com/my/repo/trunk",
|
||||
expect: expectations{errorMessage: "unsupported module: 'example.com/my/repo'"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
returnContent := tt.returnContent
|
||||
|
||||
if returnContent == "" {
|
||||
returnContent = fmt.Sprintf("<!DOCTYPE html>\n<html lang=\"en\"><head><meta name=\"go-import\" content=\"%s %s %s\"></head></html>", tt.goModuleURL, tt.vcs, tt.registryURL)
|
||||
}
|
||||
|
||||
goget := ClientImpl{
|
||||
HTTPClient: &httpMock{StatusCode: tt.returnStatus, ResponseBody: returnContent},
|
||||
}
|
||||
|
||||
repo, err := goget.GetRepositoryURL(tt.goModuleURL)
|
||||
|
||||
if tt.expect.errorMessage != "" {
|
||||
assert.EqualError(t, err, tt.expect.errorMessage)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expect.registryURL, repo)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type httpMock struct {
|
||||
Method string // is set during test execution
|
||||
URL string // is set before test execution
|
||||
ResponseBody string // is set before test execution
|
||||
Options piperhttp.ClientOptions // is set during test
|
||||
StatusCode int // is set during test
|
||||
Body readCloserMock // is set during test
|
||||
Header http.Header // is set during test
|
||||
}
|
||||
|
||||
func (c *httpMock) SetOptions(options piperhttp.ClientOptions) {
|
||||
c.Options = options
|
||||
}
|
||||
|
||||
func (c *httpMock) SendRequest(method string, url string, r io.Reader, header http.Header, cookies []*http.Cookie) (*http.Response, error) {
|
||||
c.Method = method
|
||||
c.URL = url
|
||||
c.Header = header
|
||||
|
||||
if r != nil {
|
||||
_, err := ioutil.ReadAll(r)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
c.Body = readCloserMock{Content: c.ResponseBody}
|
||||
res := http.Response{StatusCode: c.StatusCode, Body: &c.Body}
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
type readCloserMock struct {
|
||||
Content string
|
||||
Closed bool
|
||||
}
|
||||
|
||||
func (rc readCloserMock) Read(b []byte) (n int, err error) {
|
||||
|
||||
if len(b) < len(rc.Content) {
|
||||
// in real life we would fill the buffer according to buffer size ...
|
||||
return 0, fmt.Errorf("Buffer size (%d) not sufficient, need: %d", len(b), len(rc.Content))
|
||||
}
|
||||
copy(b, rc.Content)
|
||||
return len(rc.Content), io.EOF
|
||||
}
|
||||
|
||||
func (rc *readCloserMock) Close() error {
|
||||
rc.Closed = true
|
||||
return nil
|
||||
}
|
@ -10,6 +10,10 @@ metadata:
|
||||
If the build is successful the resulting artifact can be uploaded to e.g. a binary repository automatically.
|
||||
spec:
|
||||
inputs:
|
||||
secrets:
|
||||
- name: golangPrivateModulesGitTokenCredentialsId
|
||||
description: Jenkins 'Username with password' credentials ID containing username/password for http access to your git repos where your go private modules are stored.
|
||||
type: jenkins
|
||||
params:
|
||||
- name: buildFlags
|
||||
type: "[]string"
|
||||
@ -145,6 +149,30 @@ spec:
|
||||
- STEPS
|
||||
- STAGES
|
||||
- PARAMETERS
|
||||
- name: privateModules
|
||||
type: "string"
|
||||
description: Tells go which modules shall be considered to be private (by setting [GOPRIVATE](https://pkg.go.dev/cmd/go#hdr-Configuration_for_downloading_non_public_code)).
|
||||
scope:
|
||||
- STEPS
|
||||
- STAGES
|
||||
- PARAMETERS
|
||||
alias:
|
||||
- goprivate
|
||||
- name: privateModulesGitToken
|
||||
description: GitHub personal access token as per https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line.
|
||||
scope:
|
||||
- GENERAL
|
||||
- PARAMETERS
|
||||
- STAGES
|
||||
- STEPS
|
||||
type: string
|
||||
secret: true
|
||||
resourceRef:
|
||||
- name: golangPrivateModulesGitTokenCredentialsId
|
||||
type: secret
|
||||
- type: vaultSecret
|
||||
name: golangPrivateModulesGitTokenVaultSecret
|
||||
default: golang
|
||||
containers:
|
||||
- name: golang
|
||||
image: golang:1
|
||||
|
Reference in New Issue
Block a user