Skip to content

Commit

Permalink
feat(artifactPrepareVersion): Support custom certificates (#4617)
Browse files Browse the repository at this point in the history
* Add CertificateDownload func to certutils package

* Add customTlsCertificateLinks param for artifactPrepareVersion

* Add the possibility to provide custom certs for artifactPrepareVersion

* Update tests

* Return back build flags

* Return back build flags

* Update pkg/certutils/certutils.go

Apply suggestion from code review

Co-authored-by: Christopher Fenner <[email protected]>

---------

Co-authored-by: Christopher Fenner <[email protected]>
  • Loading branch information
vstarostin and CCFenner authored Oct 11, 2023
1 parent a50fad3 commit 14c7feb
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 21 deletions.
8 changes: 6 additions & 2 deletions cmd/artifactPrepareVersion.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"text/template"
"time"

"github.com/SAP/jenkins-library/pkg/certutils"
piperhttp "github.com/SAP/jenkins-library/pkg/http"
"github.com/SAP/jenkins-library/pkg/piperutils"

Expand Down Expand Up @@ -55,6 +56,7 @@ type artifactPrepareVersionUtils interface {
RunExecutable(e string, p ...string) error

DownloadFile(url, filename string, header netHttp.Header, cookies []*netHttp.Cookie) error
piperhttp.Sender

Glob(pattern string) (matches []string, err error)
FileExists(filename string) (bool, error)
Expand Down Expand Up @@ -203,8 +205,9 @@ func runArtifactPrepareVersion(config *artifactPrepareVersionOptions, telemetryD
}

if config.VersioningType == "cloud" {
certs, err := certutils.CertificateDownload(config.CustomTLSCertificateLinks, utils)
// commit changes and push to repository (including new version tag)
gitCommitID, err = pushChanges(config, newVersion, repository, worktree, now)
gitCommitID, err = pushChanges(config, newVersion, repository, worktree, now, certs)
if err != nil {
if strings.Contains(fmt.Sprint(err), "reference already exists") {
log.SetErrorCategory(log.ErrorCustom)
Expand Down Expand Up @@ -334,7 +337,7 @@ func initializeWorktree(gitCommit plumbing.Hash, worktree gitWorktree) error {
return nil
}

func pushChanges(config *artifactPrepareVersionOptions, newVersion string, repository gitRepository, worktree gitWorktree, t time.Time) (string, error) {
func pushChanges(config *artifactPrepareVersionOptions, newVersion string, repository gitRepository, worktree gitWorktree, t time.Time, certs []byte) (string, error) {

var commitID string

Expand All @@ -355,6 +358,7 @@ func pushChanges(config *artifactPrepareVersionOptions, newVersion string, repos

pushOptions := git.PushOptions{
RefSpecs: []gitConfig.RefSpec{gitConfig.RefSpec(ref)},
CABundle: certs,
}

currentRemoteOrigin, err := repository.Remote("origin")
Expand Down
11 changes: 11 additions & 0 deletions cmd/artifactPrepareVersion_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 11 additions & 9 deletions cmd/artifactPrepareVersion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ func (w *gitWorktreeMock) Commit(msg string, opts *git.CommitOptions) (plumbing.
type artifactPrepareVersionMockUtils struct {
*mock.ExecMockRunner
*mock.FilesMock
*mock.HttpClientMock
}

func newArtifactPrepareVersionMockUtils() *artifactPrepareVersionMockUtils {
Expand Down Expand Up @@ -619,7 +620,7 @@ func TestPushChanges(t *testing.T) {
repo := gitRepositoryMock{remote: remote}
worktree := gitWorktreeMock{commitHash: plumbing.ComputeHash(plumbing.CommitObject, []byte{1, 2, 3})}

commitID, err := pushChanges(&config, newVersion, &repo, &worktree, testTime)
commitID, err := pushChanges(&config, newVersion, &repo, &worktree, testTime, nil)
assert.NoError(t, err)
assert.Equal(t, "428ecf70bc22df0ba3dcf194b5ce53e769abab07", commitID)
assert.Equal(t, "update version 1.2.3", worktree.commitMsg)
Expand All @@ -633,10 +634,11 @@ func TestPushChanges(t *testing.T) {
config := artifactPrepareVersionOptions{CommitUserName: "Project Piper"}
repo := gitRepositoryMock{remote: remote}
worktree := gitWorktreeMock{commitHash: plumbing.ComputeHash(plumbing.CommitObject, []byte{1, 2, 3})}
customCerts := []byte("custom certs")

originalSSHAgentAuth := sshAgentAuth
sshAgentAuth = func(u string) (*ssh.PublicKeysCallback, error) { return &ssh.PublicKeysCallback{}, nil }
commitID, err := pushChanges(&config, newVersion, &repo, &worktree, testTime)
commitID, err := pushChanges(&config, newVersion, &repo, &worktree, testTime, customCerts)
sshAgentAuth = originalSSHAgentAuth

assert.NoError(t, err)
Expand All @@ -645,7 +647,7 @@ func TestPushChanges(t *testing.T) {
assert.Equal(t, &git.CommitOptions{All: true, Author: &object.Signature{Name: "Project Piper", When: testTime}}, worktree.commitOpts)
assert.Equal(t, "1.2.3", repo.tag)
assert.Equal(t, "428ecf70bc22df0ba3dcf194b5ce53e769abab07", repo.tagHash.String())
assert.Equal(t, &git.PushOptions{RefSpecs: []gitConfig.RefSpec{"refs/tags/1.2.3:refs/tags/1.2.3"}, Auth: &ssh.PublicKeysCallback{}}, repo.pushOptions)
assert.Equal(t, &git.PushOptions{RefSpecs: []gitConfig.RefSpec{"refs/tags/1.2.3:refs/tags/1.2.3"}, Auth: &ssh.PublicKeysCallback{}, CABundle: customCerts}, repo.pushOptions)
})

t.Run("success - ssh", func(t *testing.T) {
Expand All @@ -658,7 +660,7 @@ func TestPushChanges(t *testing.T) {

originalSSHAgentAuth := sshAgentAuth
sshAgentAuth = func(u string) (*ssh.PublicKeysCallback, error) { return &ssh.PublicKeysCallback{}, nil }
commitID, err := pushChanges(&config, newVersion, &repo, &worktree, testTime)
commitID, err := pushChanges(&config, newVersion, &repo, &worktree, testTime, nil)
sshAgentAuth = originalSSHAgentAuth

assert.NoError(t, err)
Expand All @@ -671,7 +673,7 @@ func TestPushChanges(t *testing.T) {
repo := gitRepositoryMock{}
worktree := gitWorktreeMock{commitError: "commit error", commitHash: plumbing.ComputeHash(plumbing.CommitObject, []byte{1, 2, 3})}

commitID, err := pushChanges(&config, newVersion, &repo, &worktree, testTime)
commitID, err := pushChanges(&config, newVersion, &repo, &worktree, testTime, nil)
assert.Equal(t, "0000000000000000000000000000000000000000", commitID)
assert.EqualError(t, err, "failed to commit new version: commit error")
})
Expand All @@ -681,7 +683,7 @@ func TestPushChanges(t *testing.T) {
repo := gitRepositoryMock{tagError: "tag error"}
worktree := gitWorktreeMock{commitHash: plumbing.ComputeHash(plumbing.CommitObject, []byte{1, 2, 3})}

commitID, err := pushChanges(&config, newVersion, &repo, &worktree, testTime)
commitID, err := pushChanges(&config, newVersion, &repo, &worktree, testTime, nil)
assert.Equal(t, "428ecf70bc22df0ba3dcf194b5ce53e769abab07", commitID)
assert.EqualError(t, err, "tag error")
})
Expand All @@ -691,7 +693,7 @@ func TestPushChanges(t *testing.T) {
repo := gitRepositoryMock{}
worktree := gitWorktreeMock{commitHash: plumbing.ComputeHash(plumbing.CommitObject, []byte{1, 2, 3})}

commitID, err := pushChanges(&config, newVersion, &repo, &worktree, testTime)
commitID, err := pushChanges(&config, newVersion, &repo, &worktree, testTime, nil)
assert.Equal(t, "428ecf70bc22df0ba3dcf194b5ce53e769abab07", commitID)
assert.EqualError(t, err, "no remote url maintained")
})
Expand Down Expand Up @@ -720,7 +722,7 @@ func TestPushChanges(t *testing.T) {

for _, test := range tt {
sshAgentAuth = test.sshAgentAuth
commitID, err := pushChanges(&config, newVersion, &test.repo, &worktree, testTime)
commitID, err := pushChanges(&config, newVersion, &test.repo, &worktree, testTime, nil)
sshAgentAuth = originalSSHAgentAuth

assert.Equal(t, "428ecf70bc22df0ba3dcf194b5ce53e769abab07", commitID)
Expand All @@ -733,7 +735,7 @@ func TestPushChanges(t *testing.T) {
repo := gitRepositoryMock{remote: remote, pushError: "push error"}
worktree := gitWorktreeMock{commitHash: plumbing.ComputeHash(plumbing.CommitObject, []byte{1, 2, 3})}

commitID, err := pushChanges(&config, newVersion, &repo, &worktree, testTime)
commitID, err := pushChanges(&config, newVersion, &repo, &worktree, testTime, nil)
assert.Equal(t, "428ecf70bc22df0ba3dcf194b5ce53e769abab07", commitID)
assert.EqualError(t, err, "push error")
})
Expand Down
40 changes: 30 additions & 10 deletions pkg/certutils/certutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"net/http"

piperhttp "github.com/SAP/jenkins-library/pkg/http"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/piperutils"
"github.com/pkg/errors"
)
Expand All @@ -22,23 +23,42 @@ func CertificateUpdate(certLinks []string, httpClient piperhttp.Sender, fileUtil
return errors.Wrapf(err, "failed to load file '%v'", caCertsFile)
}

for _, link := range certLinks {
response, err := httpClient.SendRequest(http.MethodGet, link, nil, nil, nil)
byteCerts, err := CertificateDownload(certLinks, httpClient)
if err != nil {
return err
}

caCerts = append(caCerts, byteCerts...)

err = fileUtils.FileWrite(caCertsFile, caCerts, 0644)
if err != nil {
return errors.Wrapf(err, "failed to update file '%v'", caCertsFile)
}
return nil
}

// CertificateDownload downloads certificates and returns them as a byte slice
func CertificateDownload(certLinks []string, client piperhttp.Sender) ([]byte, error) {
if len(certLinks) == 0 {
return nil, nil
}

var certs []byte
for _, certLink := range certLinks {
log.Entry().Debugf("Downloading CA certificate from URL: %s", certLink)
response, err := client.SendRequest(http.MethodGet, certLink, nil, nil, nil)
if err != nil {
return errors.Wrap(err, "failed to load certificate from url")
return nil, errors.Wrap(err, "failed to load certificate from url")
}

content, err := io.ReadAll(response.Body)
if err != nil {
return errors.Wrap(err, "error reading response")
return nil, errors.Wrap(err, "failed to read response")
}
_ = response.Body.Close()
content = append(content, []byte("\n")...)
caCerts = append(caCerts, content...)
}
err = fileUtils.FileWrite(caCertsFile, caCerts, 0644)
if err != nil {
return errors.Wrapf(err, "failed to update file '%v'", caCertsFile)
certs = append(certs, content...)
}
return nil

return certs, nil
}
45 changes: 45 additions & 0 deletions pkg/certutils/certutils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,48 @@ func TestCertificateUpdate(t *testing.T) {
})

}

func TestDownloadCACertbunde(t *testing.T) {
certLinks := []string{"https://test-link-1.com/cert-1.crt", "https://test-link-2.com/cert-2.crt"}
badCaseLink := "http://non-existing-url"

httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpmock.RegisterResponder(http.MethodGet, certLinks[0], httpmock.NewStringResponder(http.StatusOK, "testCert1"))
httpmock.RegisterResponder(http.MethodGet, certLinks[1], httpmock.NewStringResponder(http.StatusOK, "testCert2"))
httpmock.RegisterResponder(http.MethodGet, badCaseLink, httpmock.NewStringResponder(http.StatusNotFound, "not found"))

client := &piperhttp.Client{}
client.SetOptions(piperhttp.ClientOptions{MaxRetries: -1, UseDefaultTransport: true})

testTable := []struct {
name string
certsLinks []string
expected string
expectedErr string
}{
{
name: "good case",
certsLinks: certLinks,
expected: "testCert1\ntestCert2\n",
},
{
name: "no links",
},
{
name: "bad link",
certsLinks: []string{badCaseLink},
expectedErr: fmt.Sprintf("failed to load certificate from url: request to %s returned with response 404", badCaseLink),
},
}

for _, testCase := range testTable {
t.Run(testCase.name, func(t *testing.T) {
certs, err := CertificateDownload(testCase.certsLinks, client)
if err != nil {
assert.Contains(t, testCase.expectedErr, err.Error())
}
assert.Equal(t, testCase.expected, string(certs))
})
}
}
8 changes: 8 additions & 0 deletions resources/metadata/artifactPrepareVersion.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,14 @@ spec:
- cloud
- cloud_noTag
- library
- name: customTlsCertificateLinks
type: "[]string"
description: List containing download links of custom TLS certificates. This is required to ensure trusted connections to registries with custom certificates.
scope:
- GENERAL
- PARAMETERS
- STAGES
- STEPS
outputs:
resources:
- name: commonPipelineEnvironment
Expand Down

0 comments on commit 14c7feb

Please sign in to comment.