1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2025-03-03 15:02:35 +02:00

add package for Jenkins interactions (#2296)

This commit is contained in:
Christopher Fenner 2020-11-02 17:11:18 +01:00 committed by GitHub
parent 19e90f04f2
commit 8d682abc83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 634 additions and 0 deletions

1
go.mod
View File

@ -12,6 +12,7 @@ require (
github.com/Microsoft/hcsshim v0.8.10 // indirect
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect
github.com/bmatcuk/doublestar v1.3.2
github.com/bndr/gojenkins v1.0.1
github.com/containerd/containerd v1.4.1 // indirect
github.com/docker/docker v1.4.2-0.20200114201811-16a3519d870b // indirect
github.com/elliotchance/orderedmap v1.3.0

2
go.sum
View File

@ -125,6 +125,8 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/bmatcuk/doublestar v1.3.2 h1:mzUncgFmpzNUhIITFqGdZ8nUU0O7JTJzRO8VdkeLCSo=
github.com/bmatcuk/doublestar v1.3.2/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
github.com/bndr/gojenkins v1.0.1 h1:DFIuamRSmXoI/CwB44txuRf8xaHZNejZge/Lui4RYD4=
github.com/bndr/gojenkins v1.0.1/go.mod h1:J2FxlujWW87NJJrdysyctcDllRVYUONGGlHX16134P4=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=

32
pkg/jenkins/artifact.go Normal file
View File

@ -0,0 +1,32 @@
package jenkins
import (
"github.com/bndr/gojenkins"
)
// Artifact is an interface to abstract gojenkins.Artifact.
type Artifact interface {
SaveToDir(dir string) (bool, error)
GetData() ([]byte, error)
FileName() string
}
// ArtifactImpl is a wrapper struct for gojenkins.Artifact that respects the Artifact interface.
type ArtifactImpl struct {
artifact gojenkins.Artifact
}
// SaveToDir refers to the gojenkins.Artifact.SaveToDir function.
func (a *ArtifactImpl) SaveToDir(dir string) (bool, error) {
return a.artifact.SaveToDir(dir)
}
// GetData refers to the gojenkins.Artifact.GetData function.
func (a *ArtifactImpl) GetData() ([]byte, error) {
return a.artifact.GetData()
}
// FileName refers to the gojenkins.Artifact.FileName field.
func (a *ArtifactImpl) FileName() string {
return a.artifact.FileName
}

38
pkg/jenkins/build.go Normal file
View File

@ -0,0 +1,38 @@
package jenkins
import (
"errors"
"fmt"
"time"
"github.com/bndr/gojenkins"
)
// Build is an interface to abstract gojenkins.Build.
type Build interface {
GetArtifacts() []gojenkins.Artifact
IsRunning() bool
}
// WaitForBuildToFinish waits till a build is finished.
func WaitForBuildToFinish(build Build, pollInterval time.Duration) {
//TODO: handle timeout?
for build.IsRunning() {
time.Sleep(pollInterval)
//TODO: build.Poll() needed?
}
}
// FetchBuildArtifact is fetching a build artifact from a finished build with a certain name.
// Fails if build is running or no artifact is with the given name is found.
func FetchBuildArtifact(build Build, fileName string) (Artifact, error) {
if build.IsRunning() {
return &ArtifactImpl{}, errors.New("Failed to fetch artifact: Job is still running")
}
for _, artifact := range build.GetArtifacts() {
if artifact.FileName == fileName {
return &ArtifactImpl{artifact: artifact}, nil
}
}
return &ArtifactImpl{}, fmt.Errorf("Failed to fetch artifact: Artifact '%s' not found", fileName)
}

80
pkg/jenkins/build_test.go Normal file
View File

@ -0,0 +1,80 @@
package jenkins
import (
"fmt"
"testing"
"time"
"github.com/SAP/jenkins-library/pkg/jenkins/mocks"
"github.com/bndr/gojenkins"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestWaitForBuildToFinish(t *testing.T) {
t.Run("success", func(t *testing.T) {
// init
build := &mocks.Build{}
build.
On("IsRunning").Return(true).Once().
On("IsRunning").Return(false)
// test
WaitForBuildToFinish(build, time.Millisecond)
// asserts
build.AssertExpectations(t)
})
}
func TestFetchBuildArtifact(t *testing.T) {
fileName := "artifactFile.xml"
t.Run("success", func(t *testing.T) {
// init
build := &mocks.Build{}
build.On("IsRunning").Return(false)
build.On("GetArtifacts").Return(
[]gojenkins.Artifact{
gojenkins.Artifact{FileName: mock.Anything},
gojenkins.Artifact{FileName: fileName},
},
)
// test
artifact, err := FetchBuildArtifact(build, fileName)
// asserts
build.AssertExpectations(t)
assert.NoError(t, err)
assert.Equal(t, fileName, artifact.FileName())
})
t.Run("error - job running", func(t *testing.T) {
// init
build := &mocks.Build{}
build.On("IsRunning").Return(true)
// test
_, err := FetchBuildArtifact(build, fileName)
// asserts
build.AssertExpectations(t)
assert.EqualError(t, err, "Failed to fetch artifact: Job is still running")
})
t.Run("error - no artifacts", func(t *testing.T) {
// init
build := &mocks.Build{}
build.On("IsRunning").Return(false)
build.On("GetArtifacts").Return([]gojenkins.Artifact{})
// test
_, err := FetchBuildArtifact(build, fileName)
// asserts
build.AssertExpectations(t)
assert.EqualError(t, err, fmt.Sprintf("Failed to fetch artifact: Artifact '%s' not found", fileName))
})
t.Run("error - artifact not found", func(t *testing.T) {
// init
build := &mocks.Build{}
build.On("IsRunning").Return(false)
build.On("GetArtifacts").Return([]gojenkins.Artifact{gojenkins.Artifact{FileName: mock.Anything}})
// test
_, err := FetchBuildArtifact(build, fileName)
// asserts
build.AssertExpectations(t)
assert.EqualError(t, err, fmt.Sprintf("Failed to fetch artifact: Artifact '%s' not found", fileName))
})
}

55
pkg/jenkins/jenkins.go Normal file
View File

@ -0,0 +1,55 @@
package jenkins
import (
"fmt"
"net/http"
"strings"
"time"
"github.com/bndr/gojenkins"
)
// Jenkins is an interface to abstract gojenkins.Jenkins.
type Jenkins interface {
BuildJob(name string, options ...interface{}) (int64, error)
GetQueueItem(id int64) (*gojenkins.Task, error)
GetBuild(jobName string, number int64) (*gojenkins.Build, error)
}
// Instance connects to a Jenkins instance and returns a handler.
func Instance(client *http.Client, jenkinsURL, user, token string) (*gojenkins.Jenkins, error) {
return gojenkins.
CreateJenkins(client, jenkinsURL, user, token).
Init()
}
// TriggerJob starts a build for a given job name.
func TriggerJob(jenkins Jenkins, jobName string, parameters map[string]string) (*gojenkins.Task, error) {
// get job id
jobID := strings.ReplaceAll(jobName, "/", "/job/")
// start job
queueID, startBuildErr := jenkins.BuildJob(jobID, parameters)
if startBuildErr != nil {
return nil, startBuildErr
}
if queueID == 0 {
// handle rare error case where queueID is not set
// see https://github.com/bndr/gojenkins/issues/205
return nil, fmt.Errorf("Unable to queue build")
}
// get task
return jenkins.GetQueueItem(queueID)
}
// WaitForBuildToStart waits till a build is started.
func WaitForBuildToStart(jenkins Jenkins, jobName string, taskWrapper Task, pollInterval time.Duration) (*gojenkins.Build, error) {
// wait for job to start
buildNumber, taskTimedOutErr := taskWrapper.WaitToStart(pollInterval)
if taskTimedOutErr != nil {
return nil, taskTimedOutErr
}
// get job id
jobID := strings.ReplaceAll(jobName, "/", "/job/")
// get build
return jenkins.GetBuild(jobID, buildNumber)
}

View File

@ -0,0 +1,87 @@
package jenkins
import (
"fmt"
"strings"
"testing"
"time"
"github.com/SAP/jenkins-library/pkg/jenkins/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestTriggerJob(t *testing.T) {
jobName := "ContinuousDelivery/piper-library"
jobID := strings.ReplaceAll(jobName, "/", "/job/")
jobParameters := map[string]string{}
t.Run("error - task not started", func(t *testing.T) {
// init
queueID := int64(0)
jenkins := &mocks.Jenkins{}
jenkins.
On("BuildJob", jobID, map[string]string{}).
Return(queueID, fmt.Errorf(mock.Anything))
// test
task, err := TriggerJob(jenkins, jobName, jobParameters)
// asserts
jenkins.AssertExpectations(t)
assert.EqualError(t, err, mock.Anything)
assert.Nil(t, task)
})
t.Run("error - task already queued", func(t *testing.T) {
// init
queueID := int64(0)
jenkins := &mocks.Jenkins{}
jenkins.
On("BuildJob", jobID, map[string]string{}).
Return(queueID, nil)
// test
task, err := TriggerJob(jenkins, jobName, jobParameters)
// asserts
jenkins.AssertExpectations(t)
assert.EqualError(t, err, "Unable to queue build")
assert.Nil(t, task)
})
t.Run("error - task not queued", func(t *testing.T) {
// init
queueID := int64(43)
jenkins := &mocks.Jenkins{}
jenkins.Test(t)
jenkins.
On("BuildJob", jobID, map[string]string{}).
Return(queueID, nil).
On("GetQueueItem", queueID).
Return(nil, fmt.Errorf(mock.Anything))
// test
task, err := TriggerJob(jenkins, jobName, jobParameters)
// asserts
jenkins.AssertExpectations(t)
assert.EqualError(t, err, mock.Anything)
assert.Nil(t, task)
})
}
func TestWaitForBuildToStart(t *testing.T) {
jobName := "ContinuousDelivery/piper-library"
jobID := strings.ReplaceAll(jobName, "/", "/job/")
t.Run("error - build not started", func(t *testing.T) {
// init
buildNumber := int64(43)
task := &mocks.Task{}
task.On("WaitToStart", time.Millisecond).Return(buildNumber, nil)
jenkins := &mocks.Jenkins{}
jenkins.
On("GetBuild", jobID, buildNumber).
Return(nil, fmt.Errorf("Build not started"))
// test
build, err := WaitForBuildToStart(jenkins, jobName, task, time.Millisecond)
// asserts
task.AssertExpectations(t)
jenkins.AssertExpectations(t)
assert.EqualError(t, err, "Build not started")
assert.Nil(t, build)
})
}

View File

@ -0,0 +1,68 @@
// Code generated by mockery v1.0.0. DO NOT EDIT.
package mocks
import mock "github.com/stretchr/testify/mock"
// Artifact is an autogenerated mock type for the Artifact type
type Artifact struct {
mock.Mock
}
// FileName provides a mock function with given fields:
func (_m *Artifact) FileName() string {
ret := _m.Called()
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// GetData provides a mock function with given fields:
func (_m *Artifact) GetData() ([]byte, error) {
ret := _m.Called()
var r0 []byte
if rf, ok := ret.Get(0).(func() []byte); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]byte)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SaveToDir provides a mock function with given fields: dir
func (_m *Artifact) SaveToDir(dir string) (bool, error) {
ret := _m.Called(dir)
var r0 bool
if rf, ok := ret.Get(0).(func(string) bool); ok {
r0 = rf(dir)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(dir)
} else {
r1 = ret.Error(1)
}
return r0, r1
}

View File

@ -0,0 +1,44 @@
// Code generated by mockery v1.0.0. DO NOT EDIT.
package mocks
import (
gojenkins "github.com/bndr/gojenkins"
mock "github.com/stretchr/testify/mock"
)
// Build is an autogenerated mock type for the Build type
type Build struct {
mock.Mock
}
// GetArtifacts provides a mock function with given fields:
func (_m *Build) GetArtifacts() []gojenkins.Artifact {
ret := _m.Called()
var r0 []gojenkins.Artifact
if rf, ok := ret.Get(0).(func() []gojenkins.Artifact); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]gojenkins.Artifact)
}
}
return r0
}
// IsRunning provides a mock function with given fields:
func (_m *Build) IsRunning() bool {
ret := _m.Called()
var r0 bool
if rf, ok := ret.Get(0).(func() bool); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(bool)
}
return r0
}

View File

@ -0,0 +1,84 @@
// Code generated by mockery v1.0.0. DO NOT EDIT.
package mocks
import (
gojenkins "github.com/bndr/gojenkins"
mock "github.com/stretchr/testify/mock"
)
// Jenkins is an autogenerated mock type for the Jenkins type
type Jenkins struct {
mock.Mock
}
// BuildJob provides a mock function with given fields: name, options
func (_m *Jenkins) BuildJob(name string, options ...interface{}) (int64, error) {
var _ca []interface{}
_ca = append(_ca, name)
_ca = append(_ca, options...)
ret := _m.Called(_ca...)
var r0 int64
if rf, ok := ret.Get(0).(func(string, ...interface{}) int64); ok {
r0 = rf(name, options...)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, ...interface{}) error); ok {
r1 = rf(name, options...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetBuild provides a mock function with given fields: jobName, number
func (_m *Jenkins) GetBuild(jobName string, number int64) (*gojenkins.Build, error) {
ret := _m.Called(jobName, number)
var r0 *gojenkins.Build
if rf, ok := ret.Get(0).(func(string, int64) *gojenkins.Build); ok {
r0 = rf(jobName, number)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*gojenkins.Build)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int64) error); ok {
r1 = rf(jobName, number)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetQueueItem provides a mock function with given fields: id
func (_m *Jenkins) GetQueueItem(id int64) (*gojenkins.Task, error) {
ret := _m.Called(id)
var r0 *gojenkins.Task
if rf, ok := ret.Get(0).(func(int64) *gojenkins.Task); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*gojenkins.Task)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(int64) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}

91
pkg/jenkins/mocks/Task.go Normal file
View File

@ -0,0 +1,91 @@
// Code generated by mockery v1.0.0. DO NOT EDIT.
package mocks
import (
time "time"
mock "github.com/stretchr/testify/mock"
)
// Task is an autogenerated mock type for the Task type
type Task struct {
mock.Mock
}
// BuildNumber provides a mock function with given fields:
func (_m *Task) BuildNumber() (int64, error) {
ret := _m.Called()
var r0 int64
if rf, ok := ret.Get(0).(func() int64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// HasStarted provides a mock function with given fields:
func (_m *Task) HasStarted() bool {
ret := _m.Called()
var r0 bool
if rf, ok := ret.Get(0).(func() bool); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// Poll provides a mock function with given fields:
func (_m *Task) Poll() (int, error) {
ret := _m.Called()
var r0 int
if rf, ok := ret.Get(0).(func() int); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(int)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// WaitToStart provides a mock function with given fields: pollInterval
func (_m *Task) WaitToStart(pollInterval time.Duration) (int64, error) {
ret := _m.Called(pollInterval)
var r0 int64
if rf, ok := ret.Get(0).(func(time.Duration) int64); ok {
r0 = rf(pollInterval)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(time.Duration) error); ok {
r1 = rf(pollInterval)
} else {
r1 = ret.Error(1)
}
return r0, r1
}

52
pkg/jenkins/task.go Normal file
View File

@ -0,0 +1,52 @@
package jenkins
import (
"fmt"
"time"
"github.com/bndr/gojenkins"
)
// Task is an interface to abstract gojenkins.Task.
type Task interface {
Poll() (int, error)
BuildNumber() (int64, error)
HasStarted() bool
WaitToStart(pollInterval time.Duration) (int64, error)
}
// TaskImpl is a wrapper struct for gojenkins.Task that respects the Task interface.
type TaskImpl struct {
Task *gojenkins.Task
}
// Poll refers to the gojenkins.Task.Poll function.
func (t *TaskImpl) Poll() (int, error) {
return t.Task.Poll()
}
// HasStarted checks if the wrapped gojenkins.Task has started by checking the assigned executable URL.
func (t *TaskImpl) HasStarted() bool {
return t.Task.Raw.Executable.URL != ""
}
// BuildNumber returns the assigned build number or an error if the build has not yet started.
func (t *TaskImpl) BuildNumber() (int64, error) {
if !t.HasStarted() {
return 0, fmt.Errorf("build did not start yet")
}
return t.Task.Raw.Executable.Number, nil
}
// WaitToStart waits till the build has started.
func (t *TaskImpl) WaitToStart(pollInterval time.Duration) (int64, error) {
for retry := 0; retry < 15; {
if t.HasStarted() {
return t.BuildNumber()
}
time.Sleep(pollInterval)
t.Poll()
retry++
}
return 0, fmt.Errorf("build did not start in a reasonable amount of time")
}