reorganise directories for better readbility

This commit is contained in:
jkhelil
2020-05-19 10:26:18 +02:00
parent 57ad3b6be8
commit 5e1496cbc3
85 changed files with 134 additions and 133 deletions
-2
View File
@@ -1,2 +0,0 @@
// Package client contains client for Jenkins API
package client
-231
View File
@@ -1,231 +0,0 @@
package client
import (
"fmt"
"net/http"
"net/http/cookiejar"
"regexp"
"strings"
"github.com/bndr/gojenkins"
"github.com/pkg/errors"
)
var (
errorNotFound = errors.New("404")
regex = regexp.MustCompile("(<application-desc main-class=\"hudson.remoting.jnlp.Main\"><argument>)(?P<secret>[a-z0-9]*)")
)
// Jenkins defines Jenkins API.
type Jenkins interface {
GenerateToken(userName, tokenName string) (*UserToken, error)
Info() (*gojenkins.ExecutorResponse, error)
SafeRestart() error
CreateNode(name string, numExecutors int, description string, remoteFS string, label string, options ...interface{}) (*gojenkins.Node, error)
DeleteNode(name string) (bool, error)
CreateFolder(name string, parents ...string) (*gojenkins.Folder, error)
CreateJobInFolder(config string, jobName string, parentIDs ...string) (*gojenkins.Job, error)
CreateJob(config string, options ...interface{}) (*gojenkins.Job, error)
CreateOrUpdateJob(config, jobName string) (*gojenkins.Job, bool, error)
RenameJob(job string, name string) *gojenkins.Job
CopyJob(copyFrom string, newName string) (*gojenkins.Job, error)
DeleteJob(name string) (bool, error)
BuildJob(name string, options ...interface{}) (int64, error)
GetNode(name string) (*gojenkins.Node, error)
GetLabel(name string) (*gojenkins.Label, error)
GetBuild(jobName string, number int64) (*gojenkins.Build, error)
GetJob(id string, parentIDs ...string) (*gojenkins.Job, error)
GetSubJob(parentID string, childID string) (*gojenkins.Job, error)
GetFolder(id string, parents ...string) (*gojenkins.Folder, error)
GetAllNodes() ([]*gojenkins.Node, error)
GetAllBuildIds(job string) ([]gojenkins.JobBuild, error)
GetAllJobNames() ([]gojenkins.InnerJob, error)
GetAllJobs() ([]*gojenkins.Job, error)
GetQueue() (*gojenkins.Queue, error)
GetQueueUrl() string
GetQueueItem(id int64) (*gojenkins.Task, error)
GetArtifactData(id string) (*gojenkins.FingerPrintResponse, error)
GetPlugins(depth int) (*gojenkins.Plugins, error)
UninstallPlugin(name string) error
HasPlugin(name string) (*gojenkins.Plugin, error)
InstallPlugin(name string, version string) error
ValidateFingerPrint(id string) (bool, error)
GetView(name string) (*gojenkins.View, error)
GetAllViews() ([]*gojenkins.View, error)
CreateView(name string, viewType string) (*gojenkins.View, error)
Poll() (int, error)
ExecuteScript(groovyScript string) (logs string, err error)
GetNodeSecret(name string) (string, error)
}
type jenkins struct {
gojenkins.Jenkins
}
// JenkinsAPIConnectionSettings is struct that handle information about Jenkins API connection.
type JenkinsAPIConnectionSettings struct {
Hostname string
Port int
UseNodePort bool
}
type setBearerToken struct {
rt http.RoundTripper
token string
}
func (t *setBearerToken) transport() http.RoundTripper {
if t.rt != nil {
return t.rt
}
return http.DefaultTransport
}
func (t *setBearerToken) RoundTrip(r *http.Request) (*http.Response, error) {
r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", t.token))
return t.transport().RoundTrip(r)
}
// CreateOrUpdateJob creates or updates a job from config.
func (jenkins *jenkins) CreateOrUpdateJob(config, jobName string) (job *gojenkins.Job, created bool, err error) {
// create or update
job, err = jenkins.GetJob(jobName)
if isNotFoundError(err) {
job, err = jenkins.CreateJob(config, jobName)
created = true
return job, true, errors.WithStack(err)
} else if err != nil {
return job, false, errors.WithStack(err)
}
err = job.UpdateConfig(config)
return job, false, errors.WithStack(err)
}
// BuildJenkinsAPIUrl returns Jenkins API URL.
func (j JenkinsAPIConnectionSettings) BuildJenkinsAPIUrl(serviceName string, serviceNamespace string, servicePort int32, serviceNodePort int32) string {
if j.Hostname == "" && j.Port == 0 {
return fmt.Sprintf("http://%s.%s:%d", serviceName, serviceNamespace, servicePort)
}
if j.Hostname != "" && j.UseNodePort {
return fmt.Sprintf("http://%s:%d", j.Hostname, serviceNodePort)
}
return fmt.Sprintf("http://%s:%d", j.Hostname, j.Port)
}
// Validate validates jenkins API connection settings.
func (j JenkinsAPIConnectionSettings) Validate() error {
if j.Port > 0 && j.UseNodePort {
return errors.New("can't use service port and nodePort both. Please use port or nodePort")
}
if j.Port < 0 {
return errors.New("service port cannot be lower than 0")
}
if (j.Hostname == "" && j.Port > 0) || (j.Hostname == "" && j.UseNodePort) {
return errors.New("empty hostname is now allowed. Please provide hostname")
}
return nil
}
// NewUserAndPasswordAuthorization creates Jenkins API client with user and password authorization.
func NewUserAndPasswordAuthorization(url, userName, passwordOrToken string) (Jenkins, error) {
return newClient(url, userName, passwordOrToken)
}
// NewBearerTokenAuthorization creates Jenkins API client with bearer token authorization.
func NewBearerTokenAuthorization(url, token string) (Jenkins, error) {
return newClient(url, "", token)
}
func newClient(url, userName, passwordOrToken string) (Jenkins, error) {
if strings.HasSuffix(url, "/") {
url = url[:len(url)-1]
}
jenkinsClient := &jenkins{}
jenkinsClient.Server = url
var basicAuth *gojenkins.BasicAuth
jar, err := cookiejar.New(nil)
if err != nil {
return nil, errors.Wrap(err, "couldn't create a cookie jar")
}
httpClient := &http.Client{Jar: jar}
if len(userName) > 0 && len(passwordOrToken) > 0 {
basicAuth = &gojenkins.BasicAuth{Username: userName, Password: passwordOrToken}
} else {
httpClient.Transport = &setBearerToken{token: passwordOrToken, rt: httpClient.Transport}
}
jenkinsClient.Requester = &gojenkins.Requester{
Base: url,
SslVerify: true,
Client: httpClient,
BasicAuth: basicAuth,
}
if _, err := jenkinsClient.Init(); err != nil {
return nil, errors.Wrap(err, "couldn't init Jenkins API client")
}
status, err := jenkinsClient.Poll()
if err != nil {
return nil, errors.Wrap(err, "couldn't poll data from Jenkins API")
}
if status != http.StatusOK {
return nil, errors.Errorf("couldn't poll data from Jenkins API, invalid status code returned: %d", status)
}
return jenkinsClient, nil
}
func isNotFoundError(err error) bool {
if err != nil {
return err.Error() == errorNotFound.Error()
}
return false
}
func (jenkins *jenkins) GetNodeSecret(name string) (string, error) {
var content string
r, err := jenkins.Requester.GetXML(fmt.Sprintf("/computer/%s/slave-agent.jnlp", name), &content, nil)
if err != nil {
return "", errors.WithStack(err)
}
defer r.Body.Close()
match := regex.FindStringSubmatch(content)
if match == nil {
return "", errors.New("Node secret cannot be parsed")
}
result := make(map[string]string)
for i, name := range regex.SubexpNames() {
if i != 0 && name != "" {
result[name] = match[i]
}
}
return result["secret"], nil
}
// Returns the list of all plugins installed on the Jenkins server.
// You can supply depth parameter, to limit how much data is returned.
func (jenkins *jenkins) GetPlugins(depth int) (*gojenkins.Plugins, error) {
p := gojenkins.Plugins{Jenkins: &jenkins.Jenkins, Raw: new(gojenkins.PluginResponse), Base: "/pluginManager", Depth: depth}
statusCode, err := p.Poll()
if err != nil {
return nil, err
}
if statusCode != http.StatusOK {
return nil, fmt.Errorf("invalid status code returned: %d", statusCode)
}
return &p, nil
}
-30
View File
@@ -1,30 +0,0 @@
package client
import (
"net/url"
"github.com/bndr/gojenkins"
)
func (jenkins *jenkins) GetBuild(jobName string, number int64) (*gojenkins.Build, error) {
job, err := jenkins.GetJob(jobName)
if err != nil {
return nil, err
}
// https://github.com/bndr/gojenkins/issues/176
// workaround begin
jobURL, err := url.Parse(job.Raw.URL)
if err != nil {
return nil, err
}
job.Raw.URL = jobURL.RequestURI()
// workaround end
build, err := job.GetBuild(number)
if err != nil {
return nil, err
}
return build, nil
}
-634
View File
@@ -1,634 +0,0 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: pkg/controller/jenkins/client/jenkins.go
// Package mock_client is a generated GoMock package.
package client
import (
"reflect"
"github.com/bndr/gojenkins"
"github.com/golang/mock/gomock"
)
// MockJenkins is a mock of Jenkins interface
type MockJenkins struct {
ctrl *gomock.Controller
recorder *MockJenkinsMockRecorder
}
func (m *MockJenkins) GetNodeSecret(name string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetNodeSecret", name)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
func (mr *MockJenkinsMockRecorder) GetNodeSecret(name string) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNodeSecret", reflect.TypeOf((*MockJenkins)(nil).GetNodeSecret), name)
}
// MockJenkinsMockRecorder is the mock recorder for MockJenkins
type MockJenkinsMockRecorder struct {
mock *MockJenkins
}
// NewMockJenkins creates a new mock instance
func NewMockJenkins(ctrl *gomock.Controller) *MockJenkins {
mock := &MockJenkins{ctrl: ctrl}
mock.recorder = &MockJenkinsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockJenkins) EXPECT() *MockJenkinsMockRecorder {
return m.recorder
}
// GenerateToken mocks base method
func (m *MockJenkins) GenerateToken(userName, tokenName string) (*UserToken, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GenerateToken", userName, tokenName)
ret0, _ := ret[0].(*UserToken)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GenerateToken indicates an expected call of GenerateToken
func (mr *MockJenkinsMockRecorder) GenerateToken(userName, tokenName interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateToken", reflect.TypeOf((*MockJenkins)(nil).GenerateToken), userName, tokenName)
}
// Info mocks base method
func (m *MockJenkins) Info() (*gojenkins.ExecutorResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Info")
ret0, _ := ret[0].(*gojenkins.ExecutorResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Info indicates an expected call of Info
func (mr *MockJenkinsMockRecorder) Info() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockJenkins)(nil).Info))
}
// SafeRestart mocks base method
func (m *MockJenkins) SafeRestart() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SafeRestart")
ret0, _ := ret[0].(error)
return ret0
}
// SafeRestart indicates an expected call of SafeRestart
func (mr *MockJenkinsMockRecorder) SafeRestart() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SafeRestart", reflect.TypeOf((*MockJenkins)(nil).SafeRestart))
}
// CreateNode mocks base method
func (m *MockJenkins) CreateNode(name string, numExecutors int, description, remoteFS, label string, options ...interface{}) (*gojenkins.Node, error) {
m.ctrl.T.Helper()
varargs := []interface{}{name, numExecutors, description, remoteFS, label}
for _, a := range options {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "CreateNode", varargs...)
ret0, _ := ret[0].(*gojenkins.Node)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateNode indicates an expected call of CreateNode
func (mr *MockJenkinsMockRecorder) CreateNode(name, numExecutors, description, remoteFS, label interface{}, options ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{name, numExecutors, description, remoteFS, label}, options...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateNode", reflect.TypeOf((*MockJenkins)(nil).CreateNode), varargs...)
}
// DeleteNode mocks base method
func (m *MockJenkins) DeleteNode(name string) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteNode", name)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteNode indicates an expected call of DeleteNode
func (mr *MockJenkinsMockRecorder) DeleteNode(name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteNode", reflect.TypeOf((*MockJenkins)(nil).DeleteNode), name)
}
// CreateFolder mocks base method
func (m *MockJenkins) CreateFolder(name string, parents ...string) (*gojenkins.Folder, error) {
m.ctrl.T.Helper()
varargs := []interface{}{name}
for _, a := range parents {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "CreateFolder", varargs...)
ret0, _ := ret[0].(*gojenkins.Folder)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateFolder indicates an expected call of CreateFolder
func (mr *MockJenkinsMockRecorder) CreateFolder(name interface{}, parents ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{name}, parents...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateFolder", reflect.TypeOf((*MockJenkins)(nil).CreateFolder), varargs...)
}
// CreateJobInFolder mocks base method
func (m *MockJenkins) CreateJobInFolder(config, jobName string, parentIDs ...string) (*gojenkins.Job, error) {
m.ctrl.T.Helper()
varargs := []interface{}{config, jobName}
for _, a := range parentIDs {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "CreateJobInFolder", varargs...)
ret0, _ := ret[0].(*gojenkins.Job)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateJobInFolder indicates an expected call of CreateJobInFolder
func (mr *MockJenkinsMockRecorder) CreateJobInFolder(config, jobName interface{}, parentIDs ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{config, jobName}, parentIDs...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateJobInFolder", reflect.TypeOf((*MockJenkins)(nil).CreateJobInFolder), varargs...)
}
// CreateJob mocks base method
func (m *MockJenkins) CreateJob(config string, options ...interface{}) (*gojenkins.Job, error) {
m.ctrl.T.Helper()
varargs := []interface{}{config}
for _, a := range options {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "CreateJob", varargs...)
ret0, _ := ret[0].(*gojenkins.Job)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateJob indicates an expected call of CreateJob
func (mr *MockJenkinsMockRecorder) CreateJob(config interface{}, options ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{config}, options...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateJob", reflect.TypeOf((*MockJenkins)(nil).CreateJob), varargs...)
}
// CreateOrUpdateJob mocks base method
func (m *MockJenkins) CreateOrUpdateJob(config, jobName string) (*gojenkins.Job, bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateOrUpdateJob", config, jobName)
ret0, _ := ret[0].(*gojenkins.Job)
ret1, _ := ret[1].(bool)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// CreateOrUpdateJob indicates an expected call of CreateOrUpdateJob
func (mr *MockJenkinsMockRecorder) CreateOrUpdateJob(config, jobName interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdateJob", reflect.TypeOf((*MockJenkins)(nil).CreateOrUpdateJob), config, jobName)
}
// RenameJob mocks base method
func (m *MockJenkins) RenameJob(job, name string) *gojenkins.Job {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RenameJob", job, name)
ret0, _ := ret[0].(*gojenkins.Job)
return ret0
}
// RenameJob indicates an expected call of RenameJob
func (mr *MockJenkinsMockRecorder) RenameJob(job, name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RenameJob", reflect.TypeOf((*MockJenkins)(nil).RenameJob), job, name)
}
// CopyJob mocks base method
func (m *MockJenkins) CopyJob(copyFrom, newName string) (*gojenkins.Job, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CopyJob", copyFrom, newName)
ret0, _ := ret[0].(*gojenkins.Job)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CopyJob indicates an expected call of CopyJob
func (mr *MockJenkinsMockRecorder) CopyJob(copyFrom, newName interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CopyJob", reflect.TypeOf((*MockJenkins)(nil).CopyJob), copyFrom, newName)
}
// DeleteJob mocks base method
func (m *MockJenkins) DeleteJob(name string) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteJob", name)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteJob indicates an expected call of DeleteJob
func (mr *MockJenkinsMockRecorder) DeleteJob(name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteJob", reflect.TypeOf((*MockJenkins)(nil).DeleteJob), name)
}
// BuildJob mocks base method
func (m *MockJenkins) BuildJob(name string, options ...interface{}) (int64, error) {
m.ctrl.T.Helper()
varargs := []interface{}{name}
for _, a := range options {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BuildJob", varargs...)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// BuildJob indicates an expected call of BuildJob
func (mr *MockJenkinsMockRecorder) BuildJob(name interface{}, options ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{name}, options...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BuildJob", reflect.TypeOf((*MockJenkins)(nil).BuildJob), varargs...)
}
// GetNode mocks base method
func (m *MockJenkins) GetNode(name string) (*gojenkins.Node, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetNode", name)
ret0, _ := ret[0].(*gojenkins.Node)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetNode indicates an expected call of GetNode
func (mr *MockJenkinsMockRecorder) GetNode(name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNode", reflect.TypeOf((*MockJenkins)(nil).GetNode), name)
}
// GetLabel mocks base method
func (m *MockJenkins) GetLabel(name string) (*gojenkins.Label, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetLabel", name)
ret0, _ := ret[0].(*gojenkins.Label)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetLabel indicates an expected call of GetLabel
func (mr *MockJenkinsMockRecorder) GetLabel(name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLabel", reflect.TypeOf((*MockJenkins)(nil).GetLabel), name)
}
// GetBuild mocks base method
func (m *MockJenkins) GetBuild(jobName string, number int64) (*gojenkins.Build, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetBuild", jobName, number)
ret0, _ := ret[0].(*gojenkins.Build)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetBuild indicates an expected call of GetBuild
func (mr *MockJenkinsMockRecorder) GetBuild(jobName, number interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBuild", reflect.TypeOf((*MockJenkins)(nil).GetBuild), jobName, number)
}
// GetJob mocks base method
func (m *MockJenkins) GetJob(id string, parentIDs ...string) (*gojenkins.Job, error) {
m.ctrl.T.Helper()
varargs := []interface{}{id}
for _, a := range parentIDs {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "GetJob", varargs...)
ret0, _ := ret[0].(*gojenkins.Job)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetJob indicates an expected call of GetJob
func (mr *MockJenkinsMockRecorder) GetJob(id interface{}, parentIDs ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{id}, parentIDs...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetJob", reflect.TypeOf((*MockJenkins)(nil).GetJob), varargs...)
}
// GetSubJob mocks base method
func (m *MockJenkins) GetSubJob(parentID, childID string) (*gojenkins.Job, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetSubJob", parentID, childID)
ret0, _ := ret[0].(*gojenkins.Job)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetSubJob indicates an expected call of GetSubJob
func (mr *MockJenkinsMockRecorder) GetSubJob(parentID, childID interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubJob", reflect.TypeOf((*MockJenkins)(nil).GetSubJob), parentID, childID)
}
// GetFolder mocks base method
func (m *MockJenkins) GetFolder(id string, parents ...string) (*gojenkins.Folder, error) {
m.ctrl.T.Helper()
varargs := []interface{}{id}
for _, a := range parents {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "GetFolder", varargs...)
ret0, _ := ret[0].(*gojenkins.Folder)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetFolder indicates an expected call of GetFolder
func (mr *MockJenkinsMockRecorder) GetFolder(id interface{}, parents ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{id}, parents...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFolder", reflect.TypeOf((*MockJenkins)(nil).GetFolder), varargs...)
}
// GetAllNodes mocks base method
func (m *MockJenkins) GetAllNodes() ([]*gojenkins.Node, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAllNodes")
ret0, _ := ret[0].([]*gojenkins.Node)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAllNodes indicates an expected call of GetAllNodes
func (mr *MockJenkinsMockRecorder) GetAllNodes() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllNodes", reflect.TypeOf((*MockJenkins)(nil).GetAllNodes))
}
// GetAllBuildIds mocks base method
func (m *MockJenkins) GetAllBuildIds(job string) ([]gojenkins.JobBuild, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAllBuildIds", job)
ret0, _ := ret[0].([]gojenkins.JobBuild)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAllBuildIds indicates an expected call of GetAllBuildIds
func (mr *MockJenkinsMockRecorder) GetAllBuildIds(job interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllBuildIds", reflect.TypeOf((*MockJenkins)(nil).GetAllBuildIds), job)
}
// GetAllJobNames mocks base method
func (m *MockJenkins) GetAllJobNames() ([]gojenkins.InnerJob, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAllJobNames")
ret0, _ := ret[0].([]gojenkins.InnerJob)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAllJobNames indicates an expected call of GetAllJobNames
func (mr *MockJenkinsMockRecorder) GetAllJobNames() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllJobNames", reflect.TypeOf((*MockJenkins)(nil).GetAllJobNames))
}
// GetAllJobs mocks base method
func (m *MockJenkins) GetAllJobs() ([]*gojenkins.Job, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAllJobs")
ret0, _ := ret[0].([]*gojenkins.Job)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAllJobs indicates an expected call of GetAllJobs
func (mr *MockJenkinsMockRecorder) GetAllJobs() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllJobs", reflect.TypeOf((*MockJenkins)(nil).GetAllJobs))
}
// GetQueue mocks base method
func (m *MockJenkins) GetQueue() (*gojenkins.Queue, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetQueue")
ret0, _ := ret[0].(*gojenkins.Queue)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetQueue indicates an expected call of GetQueue
func (mr *MockJenkinsMockRecorder) GetQueue() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetQueue", reflect.TypeOf((*MockJenkins)(nil).GetQueue))
}
// GetQueueUrl mocks base method
func (m *MockJenkins) GetQueueUrl() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetQueueUrl")
ret0, _ := ret[0].(string)
return ret0
}
// GetQueueUrl indicates an expected call of GetQueueUrl
func (mr *MockJenkinsMockRecorder) GetQueueUrl() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetQueueUrl", reflect.TypeOf((*MockJenkins)(nil).GetQueueUrl))
}
// GetQueueItem mocks base method
func (m *MockJenkins) GetQueueItem(id int64) (*gojenkins.Task, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetQueueItem", id)
ret0, _ := ret[0].(*gojenkins.Task)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetQueueItem indicates an expected call of GetQueueItem
func (mr *MockJenkinsMockRecorder) GetQueueItem(id interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetQueueItem", reflect.TypeOf((*MockJenkins)(nil).GetQueueItem), id)
}
// GetArtifactData mocks base method
func (m *MockJenkins) GetArtifactData(id string) (*gojenkins.FingerPrintResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetArtifactData", id)
ret0, _ := ret[0].(*gojenkins.FingerPrintResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetArtifactData indicates an expected call of GetArtifactData
func (mr *MockJenkinsMockRecorder) GetArtifactData(id interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetArtifactData", reflect.TypeOf((*MockJenkins)(nil).GetArtifactData), id)
}
// GetPlugins mocks base method
func (m *MockJenkins) GetPlugins(depth int) (*gojenkins.Plugins, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetPlugins", depth)
ret0, _ := ret[0].(*gojenkins.Plugins)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetPlugins indicates an expected call of GetPlugins
func (mr *MockJenkinsMockRecorder) GetPlugins(depth interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPlugins", reflect.TypeOf((*MockJenkins)(nil).GetPlugins), depth)
}
// UninstallPlugin mocks base method
func (m *MockJenkins) UninstallPlugin(name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UninstallPlugin", name)
ret0, _ := ret[0].(error)
return ret0
}
// UninstallPlugin indicates an expected call of UninstallPlugin
func (mr *MockJenkinsMockRecorder) UninstallPlugin(name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UninstallPlugin", reflect.TypeOf((*MockJenkins)(nil).UninstallPlugin), name)
}
// HasPlugin mocks base method
func (m *MockJenkins) HasPlugin(name string) (*gojenkins.Plugin, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HasPlugin", name)
ret0, _ := ret[0].(*gojenkins.Plugin)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HasPlugin indicates an expected call of HasPlugin
func (mr *MockJenkinsMockRecorder) HasPlugin(name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasPlugin", reflect.TypeOf((*MockJenkins)(nil).HasPlugin), name)
}
// InstallPlugin mocks base method
func (m *MockJenkins) InstallPlugin(name, version string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InstallPlugin", name, version)
ret0, _ := ret[0].(error)
return ret0
}
// InstallPlugin indicates an expected call of InstallPlugin
func (mr *MockJenkinsMockRecorder) InstallPlugin(name, version interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstallPlugin", reflect.TypeOf((*MockJenkins)(nil).InstallPlugin), name, version)
}
// ValidateFingerPrint mocks base method
func (m *MockJenkins) ValidateFingerPrint(id string) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ValidateFingerPrint", id)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ValidateFingerPrint indicates an expected call of ValidateFingerPrint
func (mr *MockJenkinsMockRecorder) ValidateFingerPrint(id interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateFingerPrint", reflect.TypeOf((*MockJenkins)(nil).ValidateFingerPrint), id)
}
// GetView mocks base method
func (m *MockJenkins) GetView(name string) (*gojenkins.View, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetView", name)
ret0, _ := ret[0].(*gojenkins.View)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetView indicates an expected call of GetView
func (mr *MockJenkinsMockRecorder) GetView(name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetView", reflect.TypeOf((*MockJenkins)(nil).GetView), name)
}
// GetAllViews mocks base method
func (m *MockJenkins) GetAllViews() ([]*gojenkins.View, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAllViews")
ret0, _ := ret[0].([]*gojenkins.View)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAllViews indicates an expected call of GetAllViews
func (mr *MockJenkinsMockRecorder) GetAllViews() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllViews", reflect.TypeOf((*MockJenkins)(nil).GetAllViews))
}
// CreateView mocks base method
func (m *MockJenkins) CreateView(name, viewType string) (*gojenkins.View, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateView", name, viewType)
ret0, _ := ret[0].(*gojenkins.View)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateView indicates an expected call of CreateView
func (mr *MockJenkinsMockRecorder) CreateView(name, viewType interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateView", reflect.TypeOf((*MockJenkins)(nil).CreateView), name, viewType)
}
// Poll mocks base method
func (m *MockJenkins) Poll() (int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Poll")
ret0, _ := ret[0].(int)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Poll indicates an expected call of Poll
func (mr *MockJenkinsMockRecorder) Poll() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Poll", reflect.TypeOf((*MockJenkins)(nil).Poll))
}
// ExecuteScript mocks base method
func (m *MockJenkins) ExecuteScript(groovyScript string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExecuteScript", groovyScript)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ExecuteScript indicates an expected call of ExecuteScript
func (mr *MockJenkinsMockRecorder) ExecuteScript(groovyScript interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteScript", reflect.TypeOf((*MockJenkins)(nil).ExecuteScript), groovyScript)
}
-62
View File
@@ -1,62 +0,0 @@
package client
import (
"bytes"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/bndr/gojenkins"
"github.com/pkg/errors"
)
// GroovyScriptExecutionFailed is custom error type which indicates passed groovy script is invalid
type GroovyScriptExecutionFailed struct {
ConfigurationType string
Source string
Name string
Logs string
}
func (e GroovyScriptExecutionFailed) Error() string {
return "script execution failed"
}
func (jenkins *jenkins) ExecuteScript(script string) (string, error) {
now := time.Now().Unix()
verifier := fmt.Sprintf("verifier-%d", now)
return jenkins.executeScript(script, verifier)
}
func (jenkins *jenkins) executeScript(script string, verifier string) (string, error) {
output := ""
fullScript := fmt.Sprintf("%s\nprint println('%s')", script, verifier)
data := url.Values{}
data.Set("script", fullScript)
ar := gojenkins.NewAPIRequest("POST", "/scriptText", bytes.NewBufferString(data.Encode()))
if err := jenkins.Requester.SetCrumb(ar); err != nil {
return output, err
}
ar.SetHeader("Content-Type", "application/x-www-form-urlencoded")
ar.Suffix = ""
r, err := jenkins.Requester.Do(ar, &output, nil)
if err != nil {
return "", errors.Wrapf(err, "couldn't execute groovy script, logs '%s'", output)
}
defer r.Body.Close()
if r.StatusCode != http.StatusOK {
return output, errors.Errorf("invalid status code '%d', logs '%s'", r.StatusCode, output)
}
if !strings.Contains(output, verifier) {
return output, &GroovyScriptExecutionFailed{}
}
return output, nil
}
@@ -1,88 +0,0 @@
package client
import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/bndr/gojenkins"
"github.com/stretchr/testify/assert"
)
const script = "some groovy code"
func Test_ExecuteScript(t *testing.T) {
verifier := "verifier-text"
t.Run("logs have verifier text", func(t *testing.T) {
ts := httptest.NewTLSServer(http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) {
if strings.Contains(request.URL.Path, "/scriptText") {
_, _ = fmt.Fprint(responseWriter, "some output\n"+verifier)
return
}
responseWriter.WriteHeader(http.StatusInternalServerError)
}))
defer ts.Close()
client := ts.Client()
jenkinsClient := &jenkins{}
jenkinsClient.Server = ts.URL
jenkinsClient.Requester = &gojenkins.Requester{
Base: ts.URL,
SslVerify: true,
Client: client,
BasicAuth: &gojenkins.BasicAuth{Username: "unused", Password: "unused"},
}
logs, err := jenkinsClient.executeScript(script, verifier)
assert.NoError(t, err, logs)
})
t.Run("logs don't have verifier text", func(t *testing.T) {
response := "some exception stack trace without verifier"
ts := httptest.NewTLSServer(http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) {
if strings.Contains(request.URL.Path, "/scriptText") {
_, _ = fmt.Fprint(responseWriter, response)
return
}
responseWriter.WriteHeader(http.StatusInternalServerError)
}))
defer ts.Close()
client := ts.Client()
jenkinsClient := &jenkins{}
jenkinsClient.Server = ts.URL
jenkinsClient.Requester = &gojenkins.Requester{
Base: ts.URL,
SslVerify: true,
Client: client,
BasicAuth: &gojenkins.BasicAuth{Username: "unused", Password: "unused"},
}
logs, err := jenkinsClient.executeScript(script, verifier)
assert.EqualError(t, err, "script execution failed", logs)
assert.Equal(t, response, logs)
})
t.Run("throw 500", func(t *testing.T) {
ts := httptest.NewTLSServer(http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) {
responseWriter.WriteHeader(http.StatusInternalServerError)
}))
defer ts.Close()
client := ts.Client()
jenkinsClient := &jenkins{}
jenkinsClient.Server = ts.URL
jenkinsClient.Requester = &gojenkins.Requester{
Base: ts.URL,
SslVerify: true,
Client: client,
BasicAuth: &gojenkins.BasicAuth{Username: "unused", Password: "unused"},
}
logs, err := jenkinsClient.executeScript(script, verifier)
assert.EqualError(t, err, "invalid status code '500', logs ''", logs)
})
}
-52
View File
@@ -1,52 +0,0 @@
package client
import (
"fmt"
"net/http"
"github.com/pkg/errors"
)
type userTokenResponseData struct {
Name string `json:"tokenName"`
UUID string `json:"tokenUuid"`
Value string `json:"tokenValue"`
}
type userTokenResponse struct {
Status string `json:"status"`
Data userTokenResponseData `json:"data"`
}
// UserToken defines user token for Jenkins API communication
type UserToken struct {
raw *userTokenResponse
base string
}
// GetToken returns user token
func (token *UserToken) GetToken() string {
return token.raw.Data.Value
}
func (jenkins *jenkins) GenerateToken(userName, tokenName string) (*UserToken, error) {
token := &UserToken{raw: new(userTokenResponse),
base: fmt.Sprintf("/user/%s/descriptorByName/jenkins.security.ApiTokenProperty/generateNewToken", userName)}
endpoint := token.base
data := map[string]string{"newTokenName": tokenName}
r, err := jenkins.Requester.Post(endpoint, nil, token.raw, data)
if err != nil {
return nil, errors.Wrap(err, "couldn't generate API token")
}
defer r.Body.Close()
if r.StatusCode == http.StatusOK {
if token.raw.Status == "ok" {
return token, nil
}
return nil, errors.New(token.raw.Status)
}
return nil, errors.Errorf("couldn't generate API token: %d", r.StatusCode)
}
@@ -1,255 +0,0 @@
package backuprestore
import (
"context"
"fmt"
"time"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources"
"github.com/jenkinsci/kubernetes-operator/pkg/log"
"github.com/go-logr/logr"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
k8s "sigs.k8s.io/controller-runtime/pkg/client"
)
type backupTrigger struct {
interval uint64
ticker *time.Ticker
}
type backupTriggers struct {
triggers map[string]backupTrigger
}
func (t *backupTriggers) stop(logger logr.Logger, namespace string, name string) {
key := t.key(namespace, name)
trigger, found := t.triggers[key]
if found {
logger.Info(fmt.Sprintf("Stopping backup trigger for '%s'", key))
trigger.ticker.Stop()
delete(t.triggers, key)
} else {
logger.V(log.VWarn).Info(fmt.Sprintf("Can't stop backup trigger for '%s', not found, skipping", key))
}
}
func (t *backupTriggers) get(namespace, name string) (backupTrigger, bool) {
trigger, found := t.triggers[t.key(namespace, name)]
return trigger, found
}
func (t *backupTriggers) key(namespace, name string) string {
return namespace + "/" + name
}
func (t *backupTriggers) add(namespace string, name string, trigger backupTrigger) {
t.triggers[t.key(namespace, name)] = trigger
}
var triggers = backupTriggers{triggers: make(map[string]backupTrigger)}
// BackupAndRestore represents Jenkins backup and restore client
type BackupAndRestore struct {
configuration.Configuration
logger logr.Logger
}
// New returns Jenkins backup and restore client
func New(configuration configuration.Configuration, logger logr.Logger) *BackupAndRestore {
return &BackupAndRestore{
Configuration: configuration,
logger: logger,
}
}
// Validate validates backup and restore configuration
func (bar *BackupAndRestore) Validate() []string {
var messages []string
allContainers := map[string]v1alpha2.Container{}
for _, container := range bar.Configuration.Jenkins.Spec.Master.Containers {
allContainers[container.Name] = container
}
restore := bar.Configuration.Jenkins.Spec.Restore
if len(restore.ContainerName) > 0 {
_, found := allContainers[restore.ContainerName]
if !found {
messages = append(messages, fmt.Sprintf("restore container '%s' not found in CR spec.master.containers", restore.ContainerName))
}
if restore.Action.Exec == nil {
messages = append(messages, "spec.restore.action.exec is not configured")
}
}
backup := bar.Configuration.Jenkins.Spec.Backup
if len(backup.ContainerName) > 0 {
_, found := allContainers[backup.ContainerName]
if !found {
messages = append(messages, fmt.Sprintf("backup container '%s' not found in CR spec.master.containers", backup.ContainerName))
}
if backup.Action.Exec == nil {
messages = append(messages, "spec.backup.action.exec is not configured")
}
if backup.Interval == 0 {
messages = append(messages, "spec.backup.interval is not configured")
}
}
if len(restore.ContainerName) > 0 && len(backup.ContainerName) == 0 {
messages = append(messages, "spec.backup.containerName is not configured")
}
if len(backup.ContainerName) > 0 && len(restore.ContainerName) == 0 {
messages = append(messages, "spec.restore.containerName is not configured")
}
return messages
}
// Restore performs Jenkins restore backup operation
func (bar *BackupAndRestore) Restore(jenkinsClient jenkinsclient.Jenkins) error {
jenkins := bar.Configuration.Jenkins
if len(jenkins.Spec.Restore.ContainerName) == 0 || jenkins.Spec.Restore.Action.Exec == nil {
bar.logger.V(log.VDebug).Info("Skipping restore backup, backup restore not configured")
return nil
}
if jenkins.Status.RestoredBackup != 0 {
bar.logger.V(log.VDebug).Info("Skipping restore backup, backup already restored")
return nil
}
if jenkins.Status.LastBackup == 0 {
bar.logger.V(log.VDebug).Info("Skipping restore backup")
if jenkins.Status.PendingBackup == 0 {
jenkins.Status.PendingBackup = 1
return bar.Client.Update(context.TODO(), jenkins)
}
return nil
}
var backupNumber uint64
if jenkins.Spec.Restore.RecoveryOnce != 0 {
backupNumber = jenkins.Spec.Restore.RecoveryOnce
} else {
backupNumber = jenkins.Status.LastBackup
}
bar.logger.Info(fmt.Sprintf("Restoring backup '%d'", backupNumber))
podName := resources.GetJenkinsMasterPodName(jenkins)
command := jenkins.Spec.Restore.Action.Exec.Command
command = append(command, fmt.Sprintf("%d", backupNumber))
_, _, err := bar.Exec(podName, jenkins.Spec.Restore.ContainerName, command)
if err == nil {
_, err := jenkinsClient.ExecuteScript("Jenkins.instance.reload()")
if err != nil {
return err
}
jenkins.Spec.Restore.RecoveryOnce = 0
jenkins.Status.RestoredBackup = backupNumber
jenkins.Status.PendingBackup = backupNumber + 1
return bar.Client.Update(context.TODO(), jenkins)
}
return err
}
// Backup performs Jenkins backup operation
func (bar *BackupAndRestore) Backup(setBackupDoneBeforePodDeletion bool) error {
jenkins := bar.Configuration.Jenkins
if len(jenkins.Spec.Backup.ContainerName) == 0 || jenkins.Spec.Backup.Action.Exec == nil {
bar.logger.V(log.VDebug).Info("Skipping restore backup, backup restore not configured")
return nil
}
if jenkins.Status.PendingBackup == jenkins.Status.LastBackup {
bar.logger.V(log.VDebug).Info("Skipping backup")
return nil
}
backupNumber := jenkins.Status.PendingBackup
bar.logger.Info(fmt.Sprintf("Performing backup '%d'", backupNumber))
podName := resources.GetJenkinsMasterPodName(jenkins)
command := jenkins.Spec.Backup.Action.Exec.Command
command = append(command, fmt.Sprintf("%d", backupNumber))
_, _, err := bar.Exec(podName, jenkins.Spec.Backup.ContainerName, command)
if err == nil {
bar.logger.V(log.VDebug).Info(fmt.Sprintf("Backup completed '%d', updating status", backupNumber))
if jenkins.Status.RestoredBackup == 0 {
jenkins.Status.RestoredBackup = backupNumber
}
jenkins.Status.LastBackup = backupNumber
jenkins.Status.PendingBackup = backupNumber
jenkins.Status.BackupDoneBeforePodDeletion = setBackupDoneBeforePodDeletion
return bar.Client.Update(context.TODO(), jenkins)
}
return err
}
func triggerBackup(ticker *time.Ticker, k8sClient k8s.Client, logger logr.Logger, namespace, name string) {
for range ticker.C {
jenkins := &v1alpha2.Jenkins{}
err := k8sClient.Get(context.TODO(), types.NamespacedName{Namespace: namespace, Name: name}, jenkins)
if err != nil && apierrors.IsNotFound(err) {
triggers.stop(logger, namespace, name)
return // abort
} else if err != nil {
logger.V(log.VWarn).Info(fmt.Sprintf("backup trigger, error when fetching CR: %s", err))
}
if jenkins.Status.LastBackup == jenkins.Status.PendingBackup {
jenkins.Status.PendingBackup++
err = k8sClient.Update(context.TODO(), jenkins)
if err != nil {
logger.V(log.VWarn).Info(fmt.Sprintf("backup trigger, error when updating CR: %s", err))
}
}
}
}
// EnsureBackupTrigger creates or update trigger which update CR to make backup
func (bar *BackupAndRestore) EnsureBackupTrigger() error {
trigger, found := triggers.get(bar.Configuration.Jenkins.Namespace, bar.Configuration.Jenkins.Name)
isBackupConfigured := len(bar.Configuration.Jenkins.Spec.Backup.ContainerName) > 0 && bar.Configuration.Jenkins.Spec.Backup.Interval > 0
if found && !isBackupConfigured {
bar.StopBackupTrigger()
return nil
}
// configured backup has no trigger
if !found && isBackupConfigured {
bar.startBackupTrigger()
return nil
}
if found && isBackupConfigured && bar.Configuration.Jenkins.Spec.Backup.Interval != trigger.interval {
bar.StopBackupTrigger()
bar.startBackupTrigger()
}
return nil
}
// StopBackupTrigger stops trigger which update CR to make backup
func (bar *BackupAndRestore) StopBackupTrigger() {
triggers.stop(bar.logger, bar.Configuration.Jenkins.Namespace, bar.Configuration.Jenkins.Name)
}
//IsBackupTriggerEnabled returns true if the backup trigger is enabled
func (bar *BackupAndRestore) IsBackupTriggerEnabled() bool {
_, enabled := triggers.get(bar.Configuration.Jenkins.Namespace, bar.Configuration.Jenkins.Name)
return enabled
}
func (bar *BackupAndRestore) startBackupTrigger() {
bar.logger.Info("Starting backup trigger")
ticker := time.NewTicker(time.Duration(bar.Configuration.Jenkins.Spec.Backup.Interval) * time.Second)
triggers.add(bar.Configuration.Jenkins.Namespace, bar.Configuration.Jenkins.Name, backupTrigger{
interval: bar.Configuration.Jenkins.Spec.Backup.Interval,
ticker: ticker,
})
go triggerBackup(ticker, bar.Client, bar.logger, bar.Configuration.Jenkins.Namespace, bar.Configuration.Jenkins.Name)
}
@@ -1,2 +0,0 @@
// Package backuprestore is responsible for create Jenkins master backup and restore it
package backuprestore
@@ -1,32 +0,0 @@
package base
import (
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources"
stackerr "github.com/pkg/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func (r *ReconcileJenkinsBaseConfiguration) createScriptsConfigMap(meta metav1.ObjectMeta) error {
configMap, err := resources.NewScriptsConfigMap(meta, r.Configuration.Jenkins)
if err != nil {
return err
}
return stackerr.WithStack(r.CreateOrUpdateResource(configMap))
}
func (r *ReconcileJenkinsBaseConfiguration) createInitConfigurationConfigMap(meta metav1.ObjectMeta) error {
configMap, err := resources.NewInitConfigurationConfigMap(meta, r.Configuration.Jenkins)
if err != nil {
return err
}
return stackerr.WithStack(r.CreateOrUpdateResource(configMap))
}
func (r *ReconcileJenkinsBaseConfiguration) createBaseConfigurationConfigMap(meta metav1.ObjectMeta) error {
configMap, err := resources.NewBaseConfigurationConfigMap(meta, r.Configuration.Jenkins)
if err != nil {
return err
}
return stackerr.WithStack(r.CreateOrUpdateResource(configMap))
}
@@ -1,93 +0,0 @@
package base
import (
"fmt"
"reflect"
corev1 "k8s.io/api/core/v1"
)
func (r *ReconcileJenkinsBaseConfiguration) compareContainers(expected corev1.Container, actual corev1.Container) (messages []string, verbose []string) {
if !reflect.DeepEqual(expected.Args, actual.Args) {
messages = append(messages, "Arguments have changed")
verbose = append(verbose, fmt.Sprintf("Arguments have changed to '%+v' in container '%s'", expected.Args, expected.Name))
}
if !reflect.DeepEqual(expected.Command, actual.Command) {
messages = append(messages, "Command has changed")
verbose = append(verbose, fmt.Sprintf("Command has changed to '%+v' in container '%s'", expected.Command, expected.Name))
}
if !compareEnv(expected.Env, actual.Env) {
messages = append(messages, "Env has changed")
verbose = append(verbose, fmt.Sprintf("Env has changed to '%+v' in container '%s'", expected.Env, expected.Name))
}
if !reflect.DeepEqual(expected.EnvFrom, actual.EnvFrom) {
messages = append(messages, "EnvFrom has changed")
verbose = append(verbose, fmt.Sprintf("EnvFrom has changed to '%+v' in container '%s'", expected.EnvFrom, expected.Name))
}
if !reflect.DeepEqual(expected.Image, actual.Image) {
messages = append(messages, "Image has changed")
verbose = append(verbose, fmt.Sprintf("Image has changed to '%+v' in container '%s'", expected.Image, expected.Name))
}
if !reflect.DeepEqual(expected.ImagePullPolicy, actual.ImagePullPolicy) {
messages = append(messages, "Image pull policy has changed")
verbose = append(verbose, fmt.Sprintf("Image pull policy has changed to '%+v' in container '%s'", expected.ImagePullPolicy, expected.Name))
}
if !reflect.DeepEqual(expected.Lifecycle, actual.Lifecycle) {
messages = append(messages, "Lifecycle has changed")
verbose = append(verbose, fmt.Sprintf("Lifecycle has changed to '%+v' in container '%s'", expected.Lifecycle, expected.Name))
}
if !reflect.DeepEqual(expected.LivenessProbe, actual.LivenessProbe) {
messages = append(messages, "Liveness probe has changed")
verbose = append(verbose, fmt.Sprintf("Liveness probe has changed to '%+v' in container '%s'", expected.LivenessProbe, expected.Name))
}
if !reflect.DeepEqual(expected.Ports, actual.Ports) {
messages = append(messages, "Ports have changed")
verbose = append(verbose, fmt.Sprintf("Ports have changed to '%+v' in container '%s'", expected.Ports, expected.Name))
}
if !reflect.DeepEqual(expected.ReadinessProbe, actual.ReadinessProbe) {
messages = append(messages, "Readiness probe has changed")
verbose = append(verbose, fmt.Sprintf("Readiness probe has changed to '%+v' in container '%s'", expected.ReadinessProbe, expected.Name))
}
if !compareContainerResources(expected.Resources, actual.Resources) {
messages = append(messages, "Resources have changed")
verbose = append(verbose, fmt.Sprintf("Resources have changed to '%+v' in container '%s'", expected.Resources, expected.Name))
}
if !reflect.DeepEqual(expected.SecurityContext, actual.SecurityContext) {
messages = append(messages, "Security context has changed")
verbose = append(verbose, fmt.Sprintf("Security context has changed to '%+v' in container '%s'", expected.SecurityContext, expected.Name))
}
if !reflect.DeepEqual(expected.WorkingDir, actual.WorkingDir) {
messages = append(messages, "Working directory has changed")
verbose = append(verbose, fmt.Sprintf("Working directory has changed to '%+v' in container '%s'", expected.WorkingDir, expected.Name))
}
if !CompareContainerVolumeMounts(expected, actual) {
messages = append(messages, "Volume mounts have changed")
verbose = append(verbose, fmt.Sprintf("Volume mounts have changed to '%+v' in container '%s'", expected.VolumeMounts, expected.Name))
}
return messages, verbose
}
func compareContainerResources(expected corev1.ResourceRequirements, actual corev1.ResourceRequirements) bool {
expectedRequestCPU, expectedRequestCPUSet := expected.Requests[corev1.ResourceCPU]
expectedRequestMemory, expectedRequestMemorySet := expected.Requests[corev1.ResourceMemory]
expectedLimitCPU, expectedLimitCPUSet := expected.Limits[corev1.ResourceCPU]
expectedLimitMemory, expectedLimitMemorySet := expected.Limits[corev1.ResourceMemory]
actualRequestCPU, actualRequestCPUSet := actual.Requests[corev1.ResourceCPU]
actualRequestMemory, actualRequestMemorySet := actual.Requests[corev1.ResourceMemory]
actualLimitCPU, actualLimitCPUSet := actual.Limits[corev1.ResourceCPU]
actualLimitMemory, actualLimitMemorySet := actual.Limits[corev1.ResourceMemory]
if expectedRequestCPUSet && (!actualRequestCPUSet || expectedRequestCPU.String() != actualRequestCPU.String()) {
return false
}
if expectedRequestMemorySet && (!actualRequestMemorySet || expectedRequestMemory.String() != actualRequestMemory.String()) {
return false
}
if expectedLimitCPUSet && (!actualLimitCPUSet || expectedLimitCPU.String() != actualLimitCPU.String()) {
return false
}
if expectedLimitMemorySet && (!actualLimitMemorySet || expectedLimitMemory.String() != actualLimitMemory.String()) {
return false
}
return true
}
@@ -1,55 +0,0 @@
package base
import (
"context"
"fmt"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/event"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/reason"
"github.com/jenkinsci/kubernetes-operator/version"
stackerr "github.com/pkg/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
func (r *ReconcileJenkinsBaseConfiguration) ensureJenkinsDeployment(meta metav1.ObjectMeta) (reconcile.Result, error) {
userAndPasswordHash, err := r.calculateUserAndPasswordHash()
if err != nil {
return reconcile.Result{}, err
}
_, err = r.GetJenkinsDeployment()
if apierrors.IsNotFound(err) {
jenkinsDeployment := resources.NewJenkinsDeployment(meta, r.Configuration.Jenkins)
*r.Notifications <- event.Event{
Jenkins: *r.Configuration.Jenkins,
Phase: event.PhaseBase,
Level: v1alpha2.NotificationLevelInfo,
Reason: reason.NewPodCreation(reason.OperatorSource, []string{"Creating a Jenkins Deployment"}),
}
r.logger.Info(fmt.Sprintf("Creating a new Jenkins Deployment %s/%s", jenkinsDeployment.Namespace, jenkinsDeployment.Name))
err := r.CreateResource(jenkinsDeployment)
if err != nil {
return reconcile.Result{}, stackerr.WithStack(err)
}
now := metav1.Now()
r.Configuration.Jenkins.Status = v1alpha2.JenkinsStatus{
OperatorVersion: version.Version,
ProvisionStartTime: &now,
LastBackup: r.Configuration.Jenkins.Status.LastBackup,
PendingBackup: r.Configuration.Jenkins.Status.LastBackup,
UserAndPasswordHash: userAndPasswordHash,
}
return reconcile.Result{Requeue: true}, r.Client.Update(context.TODO(), r.Configuration.Jenkins)
} else if err != nil && !apierrors.IsNotFound(err) {
return reconcile.Result{}, stackerr.WithStack(err)
}
return reconcile.Result{}, nil
}
@@ -1,2 +0,0 @@
// Package base is responsible for create Jenkins master pod and it's base configuration
package base
@@ -1,59 +0,0 @@
package base
import (
"context"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources"
stackerr "github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
)
func (r *ReconcileJenkinsBaseConfiguration) addLabelForWatchesResources(customization v1alpha2.Customization) error {
labelsForWatchedResources := resources.BuildLabelsForWatchedResources(*r.Configuration.Jenkins)
if len(customization.Secret.Name) > 0 {
secret := &corev1.Secret{}
err := r.Client.Get(context.TODO(), types.NamespacedName{Name: customization.Secret.Name, Namespace: r.Configuration.Jenkins.Namespace}, secret)
if err != nil {
return stackerr.WithStack(err)
}
if !resources.VerifyIfLabelsAreSet(secret, labelsForWatchedResources) {
if len(secret.ObjectMeta.Labels) == 0 {
secret.ObjectMeta.Labels = map[string]string{}
}
for key, value := range labelsForWatchedResources {
secret.ObjectMeta.Labels[key] = value
}
if err = r.Client.Update(context.TODO(), secret); err != nil {
return stackerr.WithStack(r.Client.Update(context.TODO(), secret))
}
}
}
for _, configMapRef := range customization.Configurations {
configMap := &corev1.ConfigMap{}
err := r.Client.Get(context.TODO(), types.NamespacedName{Name: configMapRef.Name, Namespace: r.Configuration.Jenkins.Namespace}, configMap)
if err != nil {
return stackerr.WithStack(err)
}
if !resources.VerifyIfLabelsAreSet(configMap, labelsForWatchedResources) {
if len(configMap.ObjectMeta.Labels) == 0 {
configMap.ObjectMeta.Labels = map[string]string{}
}
for key, value := range labelsForWatchedResources {
configMap.ObjectMeta.Labels[key] = value
}
if err = r.Client.Update(context.TODO(), configMap); err != nil {
return stackerr.WithStack(r.Client.Update(context.TODO(), configMap))
}
}
}
return nil
}
@@ -1,68 +0,0 @@
package base
import (
"fmt"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/plugins"
"github.com/jenkinsci/kubernetes-operator/pkg/log"
"github.com/bndr/gojenkins"
stackerr "github.com/pkg/errors"
)
func (r *ReconcileJenkinsBaseConfiguration) verifyPlugins(jenkinsClient jenkinsclient.Jenkins) (bool, error) {
allPluginsInJenkins, err := jenkinsClient.GetPlugins(fetchAllPlugins)
if err != nil {
return false, stackerr.WithStack(err)
}
var installedPlugins []string
for _, jenkinsPlugin := range allPluginsInJenkins.Raw.Plugins {
if isValidPlugin(jenkinsPlugin) {
installedPlugins = append(installedPlugins, plugins.Plugin{Name: jenkinsPlugin.ShortName, Version: jenkinsPlugin.Version}.String())
}
}
r.logger.V(log.VDebug).Info(fmt.Sprintf("Installed plugins '%+v'", installedPlugins))
status := true
allRequiredPlugins := [][]v1alpha2.Plugin{r.Configuration.Jenkins.Spec.Master.BasePlugins, r.Configuration.Jenkins.Spec.Master.Plugins}
for _, requiredPlugins := range allRequiredPlugins {
for _, plugin := range requiredPlugins {
if _, ok := isPluginInstalled(allPluginsInJenkins, plugin); !ok {
r.logger.V(log.VWarn).Info(fmt.Sprintf("Missing plugin '%s'", plugin))
status = false
continue
}
if found, ok := isPluginVersionCompatible(allPluginsInJenkins, plugin); !ok {
r.logger.V(log.VWarn).Info(fmt.Sprintf("Incompatible plugin '%s' version, actual '%+v'", plugin, found.Version))
status = false
}
}
}
return status, nil
}
func isPluginVersionCompatible(plugins *gojenkins.Plugins, plugin v1alpha2.Plugin) (gojenkins.Plugin, bool) {
p := plugins.Contains(plugin.Name)
if p == nil {
return gojenkins.Plugin{}, false
}
return *p, p.Version == plugin.Version
}
func isValidPlugin(plugin gojenkins.Plugin) bool {
return plugin.Active && plugin.Enabled && !plugin.Deleted
}
func isPluginInstalled(plugins *gojenkins.Plugins, requiredPlugin v1alpha2.Plugin) (gojenkins.Plugin, bool) {
p := plugins.Contains(requiredPlugin.Name)
if p == nil {
return gojenkins.Plugin{}, false
}
return *p, isValidPlugin(*p)
}
@@ -1,218 +0,0 @@
package base
import (
"context"
"fmt"
"reflect"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/backuprestore"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/event"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/reason"
"github.com/jenkinsci/kubernetes-operator/pkg/log"
"github.com/jenkinsci/kubernetes-operator/version"
stackerr "github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
func (r *ReconcileJenkinsBaseConfiguration) checkForPodRecreation(currentJenkinsMasterPod corev1.Pod, userAndPasswordHash string) reason.Reason {
var messages []string
var verbose []string
if currentJenkinsMasterPod.Status.Phase == corev1.PodFailed ||
currentJenkinsMasterPod.Status.Phase == corev1.PodSucceeded ||
currentJenkinsMasterPod.Status.Phase == corev1.PodUnknown {
//TODO add Jenkins last 10 line logs
messages = append(messages, fmt.Sprintf("Invalid Jenkins pod phase '%s'", currentJenkinsMasterPod.Status.Phase))
verbose = append(verbose, fmt.Sprintf("Invalid Jenkins pod phase '%+v'", currentJenkinsMasterPod.Status))
return reason.NewPodRestart(reason.KubernetesSource, messages, verbose...)
}
userAndPasswordHashIsDifferent := userAndPasswordHash != r.Configuration.Jenkins.Status.UserAndPasswordHash
userAndPasswordHashStatusNotEmpty := r.Configuration.Jenkins.Status.UserAndPasswordHash != ""
if userAndPasswordHashIsDifferent && userAndPasswordHashStatusNotEmpty {
messages = append(messages, "User or password have changed")
verbose = append(verbose, "User or password have changed, recreating pod")
}
if r.Configuration.Jenkins.Spec.Restore.RecoveryOnce != 0 && r.Configuration.Jenkins.Status.RestoredBackup != 0 {
messages = append(messages, "spec.restore.recoveryOnce is set")
verbose = append(verbose, "spec.restore.recoveryOnce is set, recreating pod")
}
if version.Version != r.Configuration.Jenkins.Status.OperatorVersion {
messages = append(messages, "Jenkins Operator version has changed")
verbose = append(verbose, fmt.Sprintf("Jenkins Operator version has changed, actual '%+v' new '%+v'",
r.Configuration.Jenkins.Status.OperatorVersion, version.Version))
}
if !reflect.DeepEqual(r.Configuration.Jenkins.Spec.Master.SecurityContext, currentJenkinsMasterPod.Spec.SecurityContext) {
messages = append(messages, "Jenkins pod security context has changed")
verbose = append(verbose, fmt.Sprintf("Jenkins pod security context has changed, actual '%+v' required '%+v'",
currentJenkinsMasterPod.Spec.SecurityContext, r.Configuration.Jenkins.Spec.Master.SecurityContext))
}
if !compareImagePullSecrets(r.Configuration.Jenkins.Spec.Master.ImagePullSecrets, currentJenkinsMasterPod.Spec.ImagePullSecrets) {
messages = append(messages, "Jenkins Pod ImagePullSecrets has changed")
verbose = append(verbose, fmt.Sprintf("Jenkins Pod ImagePullSecrets has changed, actual '%+v' required '%+v'",
currentJenkinsMasterPod.Spec.ImagePullSecrets, r.Configuration.Jenkins.Spec.Master.ImagePullSecrets))
}
if !compareMap(r.Configuration.Jenkins.Spec.Master.NodeSelector, currentJenkinsMasterPod.Spec.NodeSelector) {
messages = append(messages, "Jenkins pod node selector has changed")
verbose = append(verbose, fmt.Sprintf("Jenkins pod node selector has changed, actual '%+v' required '%+v'",
currentJenkinsMasterPod.Spec.NodeSelector, r.Configuration.Jenkins.Spec.Master.NodeSelector))
}
if !compareMap(r.Configuration.Jenkins.Spec.Master.Labels, currentJenkinsMasterPod.Labels) {
messages = append(messages, "Jenkins pod labels have changed")
verbose = append(verbose, fmt.Sprintf("Jenkins pod labels have changed, actual '%+v' required '%+v'",
currentJenkinsMasterPod.Labels, r.Configuration.Jenkins.Spec.Master.Labels))
}
if !compareMap(r.Configuration.Jenkins.Spec.Master.Annotations, currentJenkinsMasterPod.ObjectMeta.Annotations) {
messages = append(messages, "Jenkins pod annotations have changed")
verbose = append(verbose, fmt.Sprintf("Jenkins pod annotations have changed, actual '%+v' required '%+v'",
currentJenkinsMasterPod.ObjectMeta.Annotations, r.Configuration.Jenkins.Spec.Master.Annotations))
}
if !r.compareVolumes(currentJenkinsMasterPod) {
messages = append(messages, "Jenkins pod volumes have changed")
verbose = append(verbose, fmt.Sprintf("Jenkins pod volumes have changed, actual '%v' required '%v'",
currentJenkinsMasterPod.Spec.Volumes, r.Configuration.Jenkins.Spec.Master.Volumes))
}
if len(r.Configuration.Jenkins.Spec.Master.Containers) != len(currentJenkinsMasterPod.Spec.Containers) {
messages = append(messages, "Jenkins amount of containers has changed")
verbose = append(verbose, fmt.Sprintf("Jenkins amount of containers has changed, actual '%+v' required '%+v'",
len(currentJenkinsMasterPod.Spec.Containers), len(r.Configuration.Jenkins.Spec.Master.Containers)))
}
if r.Configuration.Jenkins.Spec.Master.PriorityClassName != currentJenkinsMasterPod.Spec.PriorityClassName {
messages = append(messages, "Jenkins priorityClassName has changed")
verbose = append(verbose, fmt.Sprintf("Jenkins priorityClassName has changed, actual '%+v' required '%+v'",
currentJenkinsMasterPod.Spec.PriorityClassName, r.Configuration.Jenkins.Spec.Master.PriorityClassName))
}
customResourceReplaced := (r.Configuration.Jenkins.Status.BaseConfigurationCompletedTime == nil ||
r.Configuration.Jenkins.Status.UserConfigurationCompletedTime == nil) &&
r.Configuration.Jenkins.Status.UserAndPasswordHash == ""
if customResourceReplaced {
messages = append(messages, "Jenkins CR has been replaced")
verbose = append(verbose, "Jenkins CR has been replaced")
}
for _, actualContainer := range currentJenkinsMasterPod.Spec.Containers {
if actualContainer.Name == resources.JenkinsMasterContainerName {
containerMessages, verboseMessages := r.compareContainers(resources.NewJenkinsMasterContainer(r.Configuration.Jenkins), actualContainer)
messages = append(messages, containerMessages...)
verbose = append(verbose, verboseMessages...)
continue
}
var expectedContainer *corev1.Container
for _, jenkinsContainer := range r.Configuration.Jenkins.Spec.Master.Containers {
if jenkinsContainer.Name == actualContainer.Name {
tmp := resources.ConvertJenkinsContainerToKubernetesContainer(jenkinsContainer)
expectedContainer = &tmp
}
}
if expectedContainer == nil {
messages = append(messages, fmt.Sprintf("Container '%s' not found in pod", actualContainer.Name))
verbose = append(verbose, fmt.Sprintf("Container '%+v' not found in pod", actualContainer))
continue
}
containerMessages, verboseMessages := r.compareContainers(*expectedContainer, actualContainer)
messages = append(messages, containerMessages...)
verbose = append(verbose, verboseMessages...)
}
return reason.NewPodRestart(reason.OperatorSource, messages, verbose...)
}
func (r *ReconcileJenkinsBaseConfiguration) ensureJenkinsMasterPod(meta metav1.ObjectMeta) (reconcile.Result, error) {
userAndPasswordHash, err := r.calculateUserAndPasswordHash()
if err != nil {
return reconcile.Result{}, err
}
// Check if this Pod already exists
currentJenkinsMasterPod, err := r.Configuration.GetJenkinsMasterPod()
if err != nil && apierrors.IsNotFound(err) {
jenkinsMasterPod := resources.NewJenkinsMasterPod(meta, r.Configuration.Jenkins)
*r.Notifications <- event.Event{
Jenkins: *r.Configuration.Jenkins,
Phase: event.PhaseBase,
Level: v1alpha2.NotificationLevelInfo,
Reason: reason.NewPodCreation(reason.OperatorSource, []string{"Creating a new Jenkins Master Pod"}),
}
r.logger.Info(fmt.Sprintf("Creating a new Jenkins Master Pod %s/%s", jenkinsMasterPod.Namespace, jenkinsMasterPod.Name))
err = r.CreateResource(jenkinsMasterPod)
if err != nil {
return reconcile.Result{}, stackerr.WithStack(err)
}
currentJenkinsMasterPod, err := r.waitUntilCreateJenkinsMasterPod()
if err == nil {
r.handleAdmissionControllerChanges(currentJenkinsMasterPod)
} else {
r.logger.V(log.VWarn).Info(fmt.Sprintf("waitUntilCreateJenkinsMasterPod has failed: %s", err))
}
now := metav1.Now()
r.Configuration.Jenkins.Status = v1alpha2.JenkinsStatus{
OperatorVersion: version.Version,
ProvisionStartTime: &now,
LastBackup: r.Configuration.Jenkins.Status.LastBackup,
PendingBackup: r.Configuration.Jenkins.Status.LastBackup,
UserAndPasswordHash: userAndPasswordHash,
}
return reconcile.Result{Requeue: true}, r.Client.Update(context.TODO(), r.Configuration.Jenkins)
} else if err != nil && !apierrors.IsNotFound(err) {
return reconcile.Result{}, stackerr.WithStack(err)
}
if currentJenkinsMasterPod == nil {
return reconcile.Result{Requeue: true}, nil
}
if r.IsJenkinsTerminating(*currentJenkinsMasterPod) && r.Configuration.Jenkins.Status.UserConfigurationCompletedTime != nil {
backupAndRestore := backuprestore.New(r.Configuration, r.logger)
if backupAndRestore.IsBackupTriggerEnabled() {
backupAndRestore.StopBackupTrigger()
return reconcile.Result{Requeue: true}, nil
}
if r.Configuration.Jenkins.Spec.Backup.MakeBackupBeforePodDeletion && !r.Configuration.Jenkins.Status.BackupDoneBeforePodDeletion {
if r.Configuration.Jenkins.Status.LastBackup == r.Configuration.Jenkins.Status.PendingBackup {
r.Configuration.Jenkins.Status.PendingBackup++
}
if err = backupAndRestore.Backup(true); err != nil {
return reconcile.Result{}, err
}
}
return reconcile.Result{Requeue: true}, nil
}
if !r.IsJenkinsTerminating(*currentJenkinsMasterPod) {
restartReason := r.checkForPodRecreation(*currentJenkinsMasterPod, userAndPasswordHash)
if restartReason.HasMessages() {
for _, msg := range restartReason.Verbose() {
r.logger.Info(msg)
}
return reconcile.Result{Requeue: true}, r.Configuration.RestartJenkinsMasterPod(restartReason)
}
}
return reconcile.Result{}, nil
}
@@ -1,91 +0,0 @@
package base
import (
"context"
"fmt"
"strings"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources"
stackerr "github.com/pkg/errors"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)
func (r *ReconcileJenkinsBaseConfiguration) createRBAC(meta metav1.ObjectMeta) error {
err := r.createServiceAccount(meta)
if err != nil {
return err
}
role := resources.NewRole(meta)
err = r.CreateOrUpdateResource(role)
if err != nil {
return stackerr.WithStack(err)
}
roleBinding := resources.NewRoleBinding(meta.Name, meta.Namespace, meta.Name, rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "Role",
Name: meta.Name,
})
err = r.CreateOrUpdateResource(roleBinding)
if err != nil {
return stackerr.WithStack(err)
}
return nil
}
func (r *ReconcileJenkinsBaseConfiguration) ensureExtraRBAC(meta metav1.ObjectMeta) error {
var err error
var name string
for _, roleRef := range r.Configuration.Jenkins.Spec.Roles {
name = getExtraRoleBindingName(meta.Name, roleRef)
roleBinding := resources.NewRoleBinding(name, meta.Namespace, meta.Name, roleRef)
err = r.CreateOrUpdateResource(roleBinding)
if err != nil {
return stackerr.WithStack(err)
}
}
roleBindings := &rbacv1.RoleBindingList{}
err = r.Client.List(context.TODO(), roleBindings, client.InNamespace(r.Configuration.Jenkins.Namespace))
if err != nil {
return stackerr.WithStack(err)
}
for _, roleBinding := range roleBindings.Items {
if !strings.HasPrefix(roleBinding.Name, getExtraRoleBindingName(meta.Name, rbacv1.RoleRef{Kind: "Role"})) &&
!strings.HasPrefix(roleBinding.Name, getExtraRoleBindingName(meta.Name, rbacv1.RoleRef{Kind: "ClusterRole"})) {
continue
}
found := false
for _, roleRef := range r.Configuration.Jenkins.Spec.Roles {
name = getExtraRoleBindingName(meta.Name, roleRef)
if roleBinding.Name == name {
found = true
continue
}
}
if !found {
r.logger.Info(fmt.Sprintf("Deleting RoleBinding '%s'", roleBinding.Name))
if err = r.Client.Delete(context.TODO(), &roleBinding); err != nil {
return stackerr.WithStack(err)
}
}
}
return nil
}
func getExtraRoleBindingName(serviceAccountName string, roleRef rbacv1.RoleRef) string {
var typeName string
if roleRef.Kind == "ClusterRole" {
typeName = "cr"
} else {
typeName = "r"
}
return fmt.Sprintf("%s-%s-%s", serviceAccountName, typeName, roleRef.Name)
}
@@ -1,431 +0,0 @@
package base
import (
"context"
"crypto/sha256"
"encoding/base64"
"fmt"
"reflect"
"strings"
"time"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/groovy"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/reason"
"github.com/jenkinsci/kubernetes-operator/pkg/log"
"github.com/go-logr/logr"
stackerr "github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
const (
fetchAllPlugins = 1
)
// ReconcileJenkinsBaseConfiguration defines values required for Jenkins base configuration.
type ReconcileJenkinsBaseConfiguration struct {
configuration.Configuration
logger logr.Logger
jenkinsAPIConnectionSettings jenkinsclient.JenkinsAPIConnectionSettings
}
// New create structure which takes care of base configuration
func New(config configuration.Configuration, jenkinsAPIConnectionSettings jenkinsclient.JenkinsAPIConnectionSettings) *ReconcileJenkinsBaseConfiguration {
return &ReconcileJenkinsBaseConfiguration{
Configuration: config,
logger: log.Log.WithValues("cr", config.Jenkins.Name),
jenkinsAPIConnectionSettings: jenkinsAPIConnectionSettings,
}
}
// Reconcile takes care of base configuration.
func (r *ReconcileJenkinsBaseConfiguration) Reconcile() (reconcile.Result, jenkinsclient.Jenkins, error) {
metaObject := resources.NewResourceObjectMeta(r.Configuration.Jenkins)
// Create Necessary Resources
err := r.ensureResourcesRequiredForJenkinsPod(metaObject)
if err != nil {
return reconcile.Result{}, nil, err
}
r.logger.V(log.VDebug).Info("Kubernetes resources are present")
if useDeploymentForJenkinsMaster(r.Configuration.Jenkins) {
result, err := r.ensureJenkinsDeployment(metaObject)
if err != nil {
return reconcile.Result{}, nil, err
}
if result.Requeue {
return result, nil, nil
}
r.logger.V(log.VDebug).Info("Jenkins Deployment is present")
return result, nil, err
}
result, err := r.ensureJenkinsMasterPod(metaObject)
if err != nil {
return reconcile.Result{}, nil, err
}
if result.Requeue {
return result, nil, nil
}
r.logger.V(log.VDebug).Info("Jenkins master pod is present")
stopReconcileLoop, err := r.detectJenkinsMasterPodStartingIssues()
if err != nil {
return reconcile.Result{}, nil, err
}
if stopReconcileLoop {
return reconcile.Result{Requeue: false}, nil, nil
}
result, err = r.waitForJenkins()
if err != nil {
return reconcile.Result{}, nil, err
}
if result.Requeue {
return result, nil, nil
}
r.logger.V(log.VDebug).Info("Jenkins master pod is ready")
jenkinsClient, err := r.Configuration.GetJenkinsClient()
if err != nil {
return reconcile.Result{}, nil, err
}
r.logger.V(log.VDebug).Info("Jenkins API client set")
ok, err := r.verifyPlugins(jenkinsClient)
if err != nil {
return reconcile.Result{}, nil, err
}
if !ok {
//TODO add what plugins have been changed
message := "Some plugins have changed, restarting Jenkins"
r.logger.Info(message)
restartReason := reason.NewPodRestart(
reason.OperatorSource,
[]string{message},
)
return reconcile.Result{Requeue: true}, nil, r.Configuration.RestartJenkinsMasterPod(restartReason)
}
result, err = r.ensureBaseConfiguration(jenkinsClient)
return result, jenkinsClient, err
}
func useDeploymentForJenkinsMaster(jenkins *v1alpha2.Jenkins) bool {
if val, ok := jenkins.Annotations["jenkins.io/use-deployment"]; ok {
if val == "true" {
return true
}
}
return false
}
func (r *ReconcileJenkinsBaseConfiguration) ensureResourcesRequiredForJenkinsPod(metaObject metav1.ObjectMeta) error {
if err := r.createOperatorCredentialsSecret(metaObject); err != nil {
return err
}
r.logger.V(log.VDebug).Info("Operator credentials secret is present")
if err := r.createScriptsConfigMap(metaObject); err != nil {
return err
}
r.logger.V(log.VDebug).Info("Scripts config map is present")
if err := r.createInitConfigurationConfigMap(metaObject); err != nil {
return err
}
r.logger.V(log.VDebug).Info("Init configuration config map is present")
if err := r.createBaseConfigurationConfigMap(metaObject); err != nil {
return err
}
r.logger.V(log.VDebug).Info("Base configuration config map is present")
if err := r.addLabelForWatchesResources(r.Configuration.Jenkins.Spec.GroovyScripts.Customization); err != nil {
return err
}
r.logger.V(log.VDebug).Info("GroovyScripts Secret and ConfigMap added watched labels")
if err := r.addLabelForWatchesResources(r.Configuration.Jenkins.Spec.ConfigurationAsCode.Customization); err != nil {
return err
}
r.logger.V(log.VDebug).Info("ConfigurationAsCode Secret and ConfigMap added watched labels")
if err := r.createRBAC(metaObject); err != nil {
return err
}
r.logger.V(log.VDebug).Info("Service account, role and role binding are present")
if err := r.ensureExtraRBAC(metaObject); err != nil {
return err
}
r.logger.V(log.VDebug).Info("Extra role bindings are present")
httpServiceName := resources.GetJenkinsHTTPServiceName(r.Configuration.Jenkins)
if err := r.createService(metaObject, httpServiceName, r.Configuration.Jenkins.Spec.Service); err != nil {
return err
}
r.logger.V(log.VDebug).Info("Jenkins HTTP Service is present")
if err := r.createService(metaObject, resources.GetJenkinsSlavesServiceName(r.Configuration.Jenkins), r.Configuration.Jenkins.Spec.SlaveService); err != nil {
return err
}
r.logger.V(log.VDebug).Info("Jenkins slave Service is present")
if resources.IsRouteAPIAvailable(&r.ClientSet) {
r.logger.V(log.VDebug).Info("Route API is available. Now creating route.")
if err := r.createRoute(metaObject, httpServiceName, r.Configuration.Jenkins); err != nil {
return err
}
r.logger.V(log.VDebug).Info("Jenkins Route is present")
}
return nil
}
func (r *ReconcileJenkinsBaseConfiguration) createOperatorCredentialsSecret(meta metav1.ObjectMeta) error {
found := &corev1.Secret{}
err := r.Configuration.Client.Get(context.TODO(), types.NamespacedName{Name: resources.GetOperatorCredentialsSecretName(r.Configuration.Jenkins), Namespace: r.Configuration.Jenkins.ObjectMeta.Namespace}, found)
if err != nil && apierrors.IsNotFound(err) {
return stackerr.WithStack(r.CreateResource(resources.NewOperatorCredentialsSecret(meta, r.Configuration.Jenkins)))
} else if err != nil && !apierrors.IsNotFound(err) {
return stackerr.WithStack(err)
}
if found.Data[resources.OperatorCredentialsSecretUserNameKey] != nil &&
found.Data[resources.OperatorCredentialsSecretPasswordKey] != nil {
return nil
}
return stackerr.WithStack(r.UpdateResource(resources.NewOperatorCredentialsSecret(meta, r.Configuration.Jenkins)))
}
func (r *ReconcileJenkinsBaseConfiguration) calculateUserAndPasswordHash() (string, error) {
credentialsSecret := &corev1.Secret{}
err := r.Client.Get(context.TODO(), types.NamespacedName{Name: resources.GetOperatorCredentialsSecretName(r.Configuration.Jenkins), Namespace: r.Configuration.Jenkins.ObjectMeta.Namespace}, credentialsSecret)
if err != nil {
return "", stackerr.WithStack(err)
}
hash := sha256.New()
hash.Write(credentialsSecret.Data[resources.OperatorCredentialsSecretUserNameKey])
hash.Write(credentialsSecret.Data[resources.OperatorCredentialsSecretPasswordKey])
return base64.StdEncoding.EncodeToString(hash.Sum(nil)), nil
}
func compareImagePullSecrets(expected, actual []corev1.LocalObjectReference) bool {
for _, expected := range expected {
found := false
for _, actual := range actual {
if expected.Name == actual.Name {
found = true
break
}
}
if !found {
return false
}
}
return true
}
func compareMap(expected, actual map[string]string) bool {
for expectedKey, expectedValue := range expected {
actualValue, found := actual[expectedKey]
if !found {
return false
}
if expectedValue != actualValue {
return false
}
}
return true
}
func compareEnv(expected, actual []corev1.EnvVar) bool {
var actualEnv []corev1.EnvVar
for _, env := range actual {
if env.Name == "KUBERNETES_PORT_443_TCP_ADDR" || env.Name == "KUBERNETES_PORT" ||
env.Name == "KUBERNETES_PORT_443_TCP" || env.Name == "KUBERNETES_SERVICE_HOST" {
continue
}
actualEnv = append(actualEnv, env)
}
return reflect.DeepEqual(expected, actualEnv)
}
// CompareContainerVolumeMounts returns true if two containers volume mounts are the same.
func CompareContainerVolumeMounts(expected corev1.Container, actual corev1.Container) bool {
var withoutServiceAccount []corev1.VolumeMount
for _, volumeMount := range actual.VolumeMounts {
if volumeMount.MountPath != "/var/run/secrets/kubernetes.io/serviceaccount" {
withoutServiceAccount = append(withoutServiceAccount, volumeMount)
}
}
return reflect.DeepEqual(expected.VolumeMounts, withoutServiceAccount)
}
// compareVolumes returns true if Jenkins pod and Jenkins CR volumes are the same
func (r *ReconcileJenkinsBaseConfiguration) compareVolumes(actualPod corev1.Pod) bool {
var withoutServiceAccount []corev1.Volume
for _, volume := range actualPod.Spec.Volumes {
if !strings.HasPrefix(volume.Name, actualPod.Spec.ServiceAccountName) {
withoutServiceAccount = append(withoutServiceAccount, volume)
}
}
return reflect.DeepEqual(
append(resources.GetJenkinsMasterPodBaseVolumes(r.Configuration.Jenkins), r.Configuration.Jenkins.Spec.Master.Volumes...),
withoutServiceAccount,
)
}
func (r *ReconcileJenkinsBaseConfiguration) detectJenkinsMasterPodStartingIssues() (stopReconcileLoop bool, err error) {
jenkinsMasterPod, err := r.Configuration.GetJenkinsMasterPod()
if err != nil {
return false, err
}
if r.Configuration.Jenkins.Status.ProvisionStartTime == nil {
return true, nil
}
if jenkinsMasterPod.Status.Phase == corev1.PodPending {
timeout := r.Configuration.Jenkins.Status.ProvisionStartTime.Add(time.Minute * 2).UTC()
now := time.Now().UTC()
if now.After(timeout) {
events := &corev1.EventList{}
err = r.Client.List(context.TODO(), events, client.InNamespace(r.Configuration.Jenkins.Namespace))
if err != nil {
return false, stackerr.WithStack(err)
}
filteredEvents := r.filterEvents(*events, *jenkinsMasterPod)
if len(filteredEvents) == 0 {
return false, nil
}
r.logger.Info(fmt.Sprintf("Jenkins master pod starting timeout, events '%+v'", filteredEvents))
return true, nil
}
}
return false, nil
}
func (r *ReconcileJenkinsBaseConfiguration) filterEvents(source corev1.EventList, jenkinsMasterPod corev1.Pod) []string {
events := []string{}
for _, eventItem := range source.Items {
if r.Configuration.Jenkins.Status.ProvisionStartTime.UTC().After(eventItem.LastTimestamp.UTC()) {
continue
}
if eventItem.Type == corev1.EventTypeNormal {
continue
}
if !strings.HasPrefix(eventItem.ObjectMeta.Name, jenkinsMasterPod.Name) {
continue
}
events = append(events, fmt.Sprintf("Message: %s Subobject: %s", eventItem.Message, eventItem.InvolvedObject.FieldPath))
}
return events
}
func (r *ReconcileJenkinsBaseConfiguration) waitForJenkins() (reconcile.Result, error) {
jenkinsMasterPod, err := r.Configuration.GetJenkinsMasterPod()
if err != nil {
return reconcile.Result{}, err
}
if r.IsJenkinsTerminating(*jenkinsMasterPod) {
r.logger.V(log.VDebug).Info("Jenkins master pod is terminating")
return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 5}, nil
}
if jenkinsMasterPod.Status.Phase != corev1.PodRunning {
r.logger.V(log.VDebug).Info("Jenkins master pod not ready")
return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 5}, nil
}
containersReadyCount := 0
for _, containerStatus := range jenkinsMasterPod.Status.ContainerStatuses {
if containerStatus.State.Terminated != nil {
message := fmt.Sprintf("Container '%s' is terminated, status '%+v'", containerStatus.Name, containerStatus)
r.logger.Info(message)
restartReason := reason.NewPodRestart(
reason.KubernetesSource,
[]string{message},
)
return reconcile.Result{Requeue: true}, r.Configuration.RestartJenkinsMasterPod(restartReason)
}
if !containerStatus.Ready {
r.logger.V(log.VDebug).Info(fmt.Sprintf("Container '%s' not ready, readiness probe failed", containerStatus.Name))
} else {
containersReadyCount++
}
}
if containersReadyCount != len(jenkinsMasterPod.Status.ContainerStatuses) {
return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 5}, nil
}
return reconcile.Result{}, nil
}
func (r *ReconcileJenkinsBaseConfiguration) ensureBaseConfiguration(jenkinsClient jenkinsclient.Jenkins) (reconcile.Result, error) {
customization := v1alpha2.GroovyScripts{
Customization: v1alpha2.Customization{
Secret: v1alpha2.SecretRef{Name: ""},
Configurations: []v1alpha2.ConfigMapRef{{Name: resources.GetBaseConfigurationConfigMapName(r.Configuration.Jenkins)}},
},
}
groovyClient := groovy.New(jenkinsClient, r.Client, r.Configuration.Jenkins, "base-groovy", customization.Customization)
requeue, err := groovyClient.Ensure(func(name string) bool {
return strings.HasSuffix(name, ".groovy")
}, func(groovyScript string) string {
return groovyScript
})
return reconcile.Result{Requeue: requeue}, err
}
func (r *ReconcileJenkinsBaseConfiguration) waitUntilCreateJenkinsMasterPod() (currentJenkinsMasterPod *corev1.Pod, err error) {
currentJenkinsMasterPod, err = r.Configuration.GetJenkinsMasterPod()
for {
if err != nil && !apierrors.IsNotFound(err) {
return nil, stackerr.WithStack(err)
} else if err == nil {
break
}
currentJenkinsMasterPod, err = r.Configuration.GetJenkinsMasterPod()
time.Sleep(time.Millisecond * 10)
}
return
}
func (r *ReconcileJenkinsBaseConfiguration) handleAdmissionControllerChanges(currentJenkinsMasterPod *corev1.Pod) {
if !reflect.DeepEqual(r.Configuration.Jenkins.Spec.Master.SecurityContext, currentJenkinsMasterPod.Spec.SecurityContext) {
r.Configuration.Jenkins.Spec.Master.SecurityContext = currentJenkinsMasterPod.Spec.SecurityContext
r.logger.Info(fmt.Sprintf("The Admission controller has changed the Jenkins master pod spec.securityContext, changing the Jenkinc CR spec.master.securityContext to '%+v'", currentJenkinsMasterPod.Spec.SecurityContext))
}
for i, container := range r.Configuration.Jenkins.Spec.Master.Containers {
if !reflect.DeepEqual(container.SecurityContext, currentJenkinsMasterPod.Spec.Containers[i].SecurityContext) {
r.Configuration.Jenkins.Spec.Master.Containers[i].SecurityContext = currentJenkinsMasterPod.Spec.Containers[i].SecurityContext
r.logger.Info(fmt.Sprintf("The Admission controller has changed the securityContext, changing the Jenkins CR spec.master.containers[%s].securityContext to '+%v'", container.Name, currentJenkinsMasterPod.Spec.Containers[i].SecurityContext))
}
}
}
@@ -1,995 +0,0 @@
package base
import (
"context"
"testing"
"github.com/bndr/gojenkins"
"github.com/golang/mock/gomock"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources"
"github.com/jenkinsci/kubernetes-operator/pkg/log"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/scheme"
k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
func TestCompareContainerVolumeMounts(t *testing.T) {
t.Run("happy with service account", func(t *testing.T) {
expectedContainer := corev1.Container{
VolumeMounts: []corev1.VolumeMount{
{
Name: "volume-name",
MountPath: "/mount/path",
},
},
}
actualContainer := corev1.Container{
VolumeMounts: []corev1.VolumeMount{
{
Name: "volume-name",
MountPath: "/mount/path",
},
{
Name: "jenkins-operator-example-token-dh4r9",
MountPath: "/var/run/secrets/kubernetes.io/serviceaccount",
ReadOnly: true,
},
},
}
got := CompareContainerVolumeMounts(expectedContainer, actualContainer)
assert.True(t, got)
})
t.Run("happy without service account", func(t *testing.T) {
expectedContainer := corev1.Container{
VolumeMounts: []corev1.VolumeMount{
{
Name: "volume-name",
MountPath: "/mount/path",
},
},
}
actualContainer := corev1.Container{
VolumeMounts: []corev1.VolumeMount{
{
Name: "volume-name",
MountPath: "/mount/path",
},
},
}
got := CompareContainerVolumeMounts(expectedContainer, actualContainer)
assert.True(t, got)
})
t.Run("different volume mounts", func(t *testing.T) {
expectedContainer := corev1.Container{
VolumeMounts: []corev1.VolumeMount{
{
Name: "volume-name",
MountPath: "/mount/path",
},
},
}
actualContainer := corev1.Container{
VolumeMounts: []corev1.VolumeMount{
{
Name: "jenkins-operator-example-token-dh4r9",
MountPath: "/var/run/secrets/kubernetes.io/serviceaccount",
ReadOnly: true,
},
},
}
got := CompareContainerVolumeMounts(expectedContainer, actualContainer)
assert.False(t, got)
})
}
func TestCompareVolumes(t *testing.T) {
t.Run("defaults", func(t *testing.T) {
jenkins := &v1alpha2.Jenkins{}
pod := corev1.Pod{
Spec: corev1.PodSpec{
ServiceAccountName: "service-account-name",
Volumes: resources.GetJenkinsMasterPodBaseVolumes(jenkins),
},
}
reconciler := New(configuration.Configuration{Jenkins: jenkins}, client.JenkinsAPIConnectionSettings{})
got := reconciler.compareVolumes(pod)
assert.True(t, got)
})
t.Run("different", func(t *testing.T) {
jenkins := &v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
Volumes: []corev1.Volume{
{
Name: "added",
},
},
},
},
}
pod := corev1.Pod{
Spec: corev1.PodSpec{
ServiceAccountName: "service-account-name",
Volumes: resources.GetJenkinsMasterPodBaseVolumes(jenkins),
},
}
reconciler := New(configuration.Configuration{Jenkins: jenkins}, client.JenkinsAPIConnectionSettings{})
got := reconciler.compareVolumes(pod)
assert.False(t, got)
})
t.Run("added one volume", func(t *testing.T) {
jenkins := &v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
Volumes: []corev1.Volume{
{
Name: "added",
},
},
},
},
}
pod := corev1.Pod{
Spec: corev1.PodSpec{
ServiceAccountName: "service-account-name",
Volumes: append(resources.GetJenkinsMasterPodBaseVolumes(jenkins), corev1.Volume{Name: "added"}),
},
}
reconciler := New(configuration.Configuration{Jenkins: jenkins}, client.JenkinsAPIConnectionSettings{})
got := reconciler.compareVolumes(pod)
assert.True(t, got)
})
}
func TestReconcileJenkinsBaseConfiguration_verifyPlugins(t *testing.T) {
log.SetupLogger(true)
t.Run("happy, empty base and user plugins", func(t *testing.T) {
jenkins := &v1alpha2.Jenkins{}
r := ReconcileJenkinsBaseConfiguration{
logger: log.Log,
Configuration: configuration.Configuration{
Jenkins: jenkins,
},
}
pluginsInJenkins := &gojenkins.Plugins{
Raw: &gojenkins.PluginResponse{},
}
ctrl := gomock.NewController(t)
defer ctrl.Finish()
jenkinsClient := client.NewMockJenkins(ctrl)
jenkinsClient.EXPECT().GetPlugins(fetchAllPlugins).Return(pluginsInJenkins, nil)
got, err := r.verifyPlugins(jenkinsClient)
assert.NoError(t, err)
assert.True(t, got)
})
t.Run("happy, not empty base and user plugins", func(t *testing.T) {
jenkins := &v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
BasePlugins: []v1alpha2.Plugin{{Name: "plugin-name1", Version: "0.0.1"}},
Plugins: []v1alpha2.Plugin{{Name: "plugin-name2", Version: "0.0.1"}},
},
},
}
r := ReconcileJenkinsBaseConfiguration{
logger: log.Log,
Configuration: configuration.Configuration{
Jenkins: jenkins,
},
}
pluginsInJenkins := &gojenkins.Plugins{
Raw: &gojenkins.PluginResponse{
Plugins: []gojenkins.Plugin{
{
ShortName: "plugin-name1",
Active: true,
Deleted: false,
Enabled: true,
Version: "0.0.1",
},
{
ShortName: "plugin-name2",
Active: true,
Deleted: false,
Enabled: true,
Version: "0.0.1",
},
},
},
}
ctrl := gomock.NewController(t)
defer ctrl.Finish()
jenkinsClient := client.NewMockJenkins(ctrl)
jenkinsClient.EXPECT().GetPlugins(fetchAllPlugins).Return(pluginsInJenkins, nil)
got, err := r.verifyPlugins(jenkinsClient)
assert.NoError(t, err)
assert.True(t, got)
})
t.Run("happy, not empty base and empty user plugins", func(t *testing.T) {
jenkins := &v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
BasePlugins: []v1alpha2.Plugin{{Name: "plugin-name", Version: "0.0.1"}},
},
},
}
r := ReconcileJenkinsBaseConfiguration{
logger: log.Log,
Configuration: configuration.Configuration{
Jenkins: jenkins,
},
}
pluginsInJenkins := &gojenkins.Plugins{
Raw: &gojenkins.PluginResponse{
Plugins: []gojenkins.Plugin{
{
ShortName: "plugin-name",
Active: true,
Deleted: false,
Enabled: true,
Version: "0.0.1",
},
},
},
}
ctrl := gomock.NewController(t)
defer ctrl.Finish()
jenkinsClient := client.NewMockJenkins(ctrl)
jenkinsClient.EXPECT().GetPlugins(fetchAllPlugins).Return(pluginsInJenkins, nil)
got, err := r.verifyPlugins(jenkinsClient)
assert.NoError(t, err)
assert.True(t, got)
})
t.Run("happy, empty base and not empty user plugins", func(t *testing.T) {
jenkins := &v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
Plugins: []v1alpha2.Plugin{{Name: "plugin-name", Version: "0.0.1"}},
},
},
}
r := ReconcileJenkinsBaseConfiguration{
logger: log.Log,
Configuration: configuration.Configuration{
Jenkins: jenkins,
},
}
pluginsInJenkins := &gojenkins.Plugins{
Raw: &gojenkins.PluginResponse{
Plugins: []gojenkins.Plugin{
{
ShortName: "plugin-name",
Active: true,
Deleted: false,
Enabled: true,
Version: "0.0.1",
},
},
},
}
ctrl := gomock.NewController(t)
defer ctrl.Finish()
jenkinsClient := client.NewMockJenkins(ctrl)
jenkinsClient.EXPECT().GetPlugins(fetchAllPlugins).Return(pluginsInJenkins, nil)
got, err := r.verifyPlugins(jenkinsClient)
assert.NoError(t, err)
assert.True(t, got)
})
t.Run("happy, plugin version matter for base plugins", func(t *testing.T) {
jenkins := &v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
BasePlugins: []v1alpha2.Plugin{{Name: "plugin-name", Version: "0.0.1"}},
},
},
}
r := ReconcileJenkinsBaseConfiguration{
logger: log.Log,
Configuration: configuration.Configuration{
Jenkins: jenkins,
},
}
pluginsInJenkins := &gojenkins.Plugins{
Raw: &gojenkins.PluginResponse{
Plugins: []gojenkins.Plugin{
{
ShortName: "plugin-name",
Active: true,
Deleted: false,
Enabled: true,
Version: "0.0.2",
},
},
},
}
ctrl := gomock.NewController(t)
defer ctrl.Finish()
jenkinsClient := client.NewMockJenkins(ctrl)
jenkinsClient.EXPECT().GetPlugins(fetchAllPlugins).Return(pluginsInJenkins, nil)
got, err := r.verifyPlugins(jenkinsClient)
assert.NoError(t, err)
assert.False(t, got)
})
t.Run("plugin version matter for user plugins", func(t *testing.T) {
jenkins := &v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
Plugins: []v1alpha2.Plugin{{Name: "plugin-name", Version: "0.0.2"}},
},
},
}
r := ReconcileJenkinsBaseConfiguration{
logger: log.Log,
Configuration: configuration.Configuration{
Jenkins: jenkins,
},
}
pluginsInJenkins := &gojenkins.Plugins{
Raw: &gojenkins.PluginResponse{
Plugins: []gojenkins.Plugin{
{
ShortName: "plugin-name",
Active: true,
Deleted: false,
Enabled: true,
Version: "0.0.1",
},
},
},
}
ctrl := gomock.NewController(t)
defer ctrl.Finish()
jenkinsClient := client.NewMockJenkins(ctrl)
jenkinsClient.EXPECT().GetPlugins(fetchAllPlugins).Return(pluginsInJenkins, nil)
got, err := r.verifyPlugins(jenkinsClient)
assert.NoError(t, err)
assert.False(t, got)
})
t.Run("missing base plugin", func(t *testing.T) {
jenkins := &v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
BasePlugins: []v1alpha2.Plugin{{Name: "plugin-name", Version: "0.0.2"}},
},
},
}
r := ReconcileJenkinsBaseConfiguration{
logger: log.Log,
Configuration: configuration.Configuration{
Jenkins: jenkins,
},
}
pluginsInJenkins := &gojenkins.Plugins{
Raw: &gojenkins.PluginResponse{
Plugins: []gojenkins.Plugin{},
},
}
ctrl := gomock.NewController(t)
defer ctrl.Finish()
jenkinsClient := client.NewMockJenkins(ctrl)
jenkinsClient.EXPECT().GetPlugins(fetchAllPlugins).Return(pluginsInJenkins, nil)
got, err := r.verifyPlugins(jenkinsClient)
assert.NoError(t, err)
assert.False(t, got)
})
t.Run("missing user plugin", func(t *testing.T) {
jenkins := &v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
Plugins: []v1alpha2.Plugin{{Name: "plugin-name", Version: "0.0.2"}},
},
},
}
r := ReconcileJenkinsBaseConfiguration{
logger: log.Log,
Configuration: configuration.Configuration{
Jenkins: jenkins,
},
}
pluginsInJenkins := &gojenkins.Plugins{
Raw: &gojenkins.PluginResponse{
Plugins: []gojenkins.Plugin{},
},
}
ctrl := gomock.NewController(t)
defer ctrl.Finish()
jenkinsClient := client.NewMockJenkins(ctrl)
jenkinsClient.EXPECT().GetPlugins(fetchAllPlugins).Return(pluginsInJenkins, nil)
got, err := r.verifyPlugins(jenkinsClient)
assert.NoError(t, err)
assert.False(t, got)
})
}
func Test_compareEnv(t *testing.T) {
t.Run("empty", func(t *testing.T) {
var expected []corev1.EnvVar
var actual []corev1.EnvVar
got := compareEnv(expected, actual)
assert.True(t, got)
})
t.Run("same", func(t *testing.T) {
expected := []corev1.EnvVar{
{
Name: "name",
Value: "value",
},
}
actual := []corev1.EnvVar{
{
Name: "name",
Value: "value",
},
}
got := compareEnv(expected, actual)
assert.True(t, got)
})
t.Run("with KUBERNETES envs", func(t *testing.T) {
expected := []corev1.EnvVar{
{
Name: "name",
Value: "value",
},
}
actual := []corev1.EnvVar{
{
Name: "name",
Value: "value",
},
{
Name: "KUBERNETES_PORT_443_TCP_ADDR",
Value: "KUBERNETES_PORT_443_TCP_ADDR",
},
{
Name: "KUBERNETES_PORT",
Value: "KUBERNETES_PORT",
},
{
Name: "KUBERNETES_PORT_443_TCP",
Value: "KUBERNETES_PORT_443_TCP",
},
{
Name: "KUBERNETES_SERVICE_HOST",
Value: "KUBERNETES_SERVICE_HOST",
},
}
got := compareEnv(expected, actual)
assert.True(t, got)
})
t.Run("different", func(t *testing.T) {
expected := []corev1.EnvVar{
{
Name: "name",
Value: "value",
},
}
actual := []corev1.EnvVar{
{
Name: "name2",
Value: "value2",
},
}
got := compareEnv(expected, actual)
assert.False(t, got)
})
}
func TestCompareMap(t *testing.T) {
t.Run("empty", func(t *testing.T) {
expectedAnnotations := map[string]string{}
actualAnnotations := map[string]string{}
got := compareMap(expectedAnnotations, actualAnnotations)
assert.True(t, got)
})
t.Run("the same", func(t *testing.T) {
expectedAnnotations := map[string]string{"one": "two"}
actualAnnotations := expectedAnnotations
got := compareMap(expectedAnnotations, actualAnnotations)
assert.True(t, got)
})
t.Run("one extra annotation in pod", func(t *testing.T) {
expectedAnnotations := map[string]string{"one": "two"}
actualAnnotations := map[string]string{"one": "two", "three": "four"}
got := compareMap(expectedAnnotations, actualAnnotations)
assert.True(t, got)
})
t.Run("one missing annotation", func(t *testing.T) {
expectedAnnotations := map[string]string{"one": "two"}
actualAnnotations := map[string]string{"three": "four"}
got := compareMap(expectedAnnotations, actualAnnotations)
assert.False(t, got)
})
t.Run("one value is different", func(t *testing.T) {
expectedAnnotations := map[string]string{"one": "two"}
actualAnnotations := map[string]string{"one": "three"}
got := compareMap(expectedAnnotations, actualAnnotations)
assert.False(t, got)
})
t.Run("one missing annotation and one value is different", func(t *testing.T) {
expectedAnnotations := map[string]string{"one": "two", "missing": "something"}
actualAnnotations := map[string]string{"one": "three"}
got := compareMap(expectedAnnotations, actualAnnotations)
assert.False(t, got)
})
}
func TestCompareImagePullSecrets(t *testing.T) {
t.Run("empty", func(t *testing.T) {
var expected []corev1.LocalObjectReference
var actual []corev1.LocalObjectReference
got := compareImagePullSecrets(expected, actual)
assert.True(t, got)
})
t.Run("the same", func(t *testing.T) {
expected := []corev1.LocalObjectReference{{Name: "test"}}
actual := []corev1.LocalObjectReference{{Name: "test"}}
got := compareImagePullSecrets(expected, actual)
assert.True(t, got)
})
t.Run("one extra pull secret in pod", func(t *testing.T) {
var expected []corev1.LocalObjectReference
actual := []corev1.LocalObjectReference{{Name: "test"}}
got := compareImagePullSecrets(expected, actual)
assert.True(t, got)
})
t.Run("one missing image pull secret", func(t *testing.T) {
expected := []corev1.LocalObjectReference{{Name: "test"}}
var actual []corev1.LocalObjectReference
got := compareImagePullSecrets(expected, actual)
assert.False(t, got)
})
}
func TestEnsureExtraRBAC(t *testing.T) {
namespace := "default"
jenkinsName := "example"
log.SetupLogger(true)
fetchAllRoleBindings := func(client k8sclient.Client) (roleBindings *rbacv1.RoleBindingList, err error) {
roleBindings = &rbacv1.RoleBindingList{}
err = client.List(context.TODO(), roleBindings, k8sclient.InNamespace(namespace))
return
}
t.Run("empty", func(t *testing.T) {
// given
fakeClient := fake.NewFakeClient()
err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme)
assert.NoError(t, err)
jenkins := &v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Name: jenkinsName,
Namespace: namespace,
},
Spec: v1alpha2.JenkinsSpec{
Roles: []rbacv1.RoleRef{},
},
}
config := configuration.Configuration{
Client: fakeClient,
Jenkins: jenkins,
Scheme: scheme.Scheme,
}
reconciler := New(config, client.JenkinsAPIConnectionSettings{})
metaObject := resources.NewResourceObjectMeta(jenkins)
// when
err = reconciler.createRBAC(metaObject)
assert.NoError(t, err)
err = reconciler.ensureExtraRBAC(metaObject)
assert.NoError(t, err)
// then
roleBindings, err := fetchAllRoleBindings(fakeClient)
assert.NoError(t, err)
assert.Equal(t, 1, len(roleBindings.Items))
assert.Equal(t, metaObject.Name, roleBindings.Items[0].Name)
})
clusterRoleKind := "ClusterRole"
t.Run("one extra", func(t *testing.T) {
// given
fakeClient := fake.NewFakeClient()
err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme)
assert.NoError(t, err)
jenkins := &v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Name: jenkinsName,
Namespace: namespace,
},
Spec: v1alpha2.JenkinsSpec{
Roles: []rbacv1.RoleRef{
{APIGroup: "rbac.authorization.k8s.io",
Kind: clusterRoleKind,
Name: "edit",
},
},
},
}
config := configuration.Configuration{
Client: fakeClient,
Jenkins: jenkins,
Scheme: scheme.Scheme,
}
reconciler := New(config, client.JenkinsAPIConnectionSettings{})
metaObject := resources.NewResourceObjectMeta(jenkins)
// when
err = reconciler.createRBAC(metaObject)
assert.NoError(t, err)
err = reconciler.ensureExtraRBAC(metaObject)
assert.NoError(t, err)
// then
roleBindings, err := fetchAllRoleBindings(fakeClient)
assert.NoError(t, err)
assert.Equal(t, 2, len(roleBindings.Items))
assert.Equal(t, metaObject.Name, roleBindings.Items[0].Name)
assert.Equal(t, jenkins.Spec.Roles[0], roleBindings.Items[1].RoleRef)
})
t.Run("two extra", func(t *testing.T) {
// given
fakeClient := fake.NewFakeClient()
err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme)
assert.NoError(t, err)
jenkins := &v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Name: jenkinsName,
Namespace: namespace,
},
Spec: v1alpha2.JenkinsSpec{
Roles: []rbacv1.RoleRef{
{
APIGroup: "rbac.authorization.k8s.io",
Kind: clusterRoleKind,
Name: "admin",
},
{
APIGroup: "rbac.authorization.k8s.io",
Kind: clusterRoleKind,
Name: "edit",
},
},
},
}
config := configuration.Configuration{
Client: fakeClient,
Jenkins: jenkins,
Scheme: scheme.Scheme,
}
reconciler := New(config, client.JenkinsAPIConnectionSettings{})
metaObject := resources.NewResourceObjectMeta(jenkins)
// when
err = reconciler.createRBAC(metaObject)
assert.NoError(t, err)
err = reconciler.ensureExtraRBAC(metaObject)
assert.NoError(t, err)
// then
roleBindings, err := fetchAllRoleBindings(fakeClient)
assert.NoError(t, err)
assert.Equal(t, 3, len(roleBindings.Items))
assert.Equal(t, metaObject.Name, roleBindings.Items[0].Name)
assert.Equal(t, jenkins.Spec.Roles[0], roleBindings.Items[1].RoleRef)
assert.Equal(t, jenkins.Spec.Roles[1], roleBindings.Items[2].RoleRef)
})
t.Run("delete one extra", func(t *testing.T) {
// given
fakeClient := fake.NewFakeClient()
err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme)
assert.NoError(t, err)
jenkins := &v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Name: jenkinsName,
Namespace: namespace,
},
Spec: v1alpha2.JenkinsSpec{
Roles: []rbacv1.RoleRef{
{
APIGroup: "rbac.authorization.k8s.io",
Kind: clusterRoleKind,
Name: "admin",
},
{
APIGroup: "rbac.authorization.k8s.io",
Kind: clusterRoleKind,
Name: "edit",
},
},
},
}
config := configuration.Configuration{
Client: fakeClient,
Jenkins: jenkins,
Scheme: scheme.Scheme,
}
reconciler := New(config, client.JenkinsAPIConnectionSettings{})
metaObject := resources.NewResourceObjectMeta(jenkins)
// when
roleBindingSkipMe := resources.NewRoleBinding("skip-me", namespace, metaObject.Name, rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: clusterRoleKind,
Name: "edit",
})
err = reconciler.CreateOrUpdateResource(roleBindingSkipMe)
assert.NoError(t, err)
err = reconciler.createRBAC(metaObject)
assert.NoError(t, err)
err = reconciler.ensureExtraRBAC(metaObject)
assert.NoError(t, err)
jenkins.Spec.Roles = []rbacv1.RoleRef{
{
APIGroup: "rbac.authorization.k8s.io",
Kind: clusterRoleKind,
Name: "admin",
},
}
err = reconciler.ensureExtraRBAC(metaObject)
assert.NoError(t, err)
// then
roleBindings, err := fetchAllRoleBindings(fakeClient)
assert.NoError(t, err)
assert.Equal(t, 3, len(roleBindings.Items))
assert.Equal(t, metaObject.Name, roleBindings.Items[1].Name)
assert.Equal(t, jenkins.Spec.Roles[0], roleBindings.Items[2].RoleRef)
})
}
func TestCompareContainerResources(t *testing.T) {
t.Run("empty", func(t *testing.T) {
var expected corev1.ResourceRequirements
var actual corev1.ResourceRequirements
got := compareContainerResources(expected, actual)
assert.True(t, got)
})
t.Run("expected resources empty, actual resources set", func(t *testing.T) {
var expected corev1.ResourceRequirements
actual := corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("50m"),
corev1.ResourceMemory: resource.MustParse("50Mi"),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("100m"),
corev1.ResourceMemory: resource.MustParse("100Mi"),
},
}
got := compareContainerResources(expected, actual)
assert.True(t, got)
})
t.Run("request CPU the same values", func(t *testing.T) {
actual := corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("50m"),
},
}
expected := actual
got := compareContainerResources(expected, actual)
assert.True(t, got)
})
t.Run("request memory the same values", func(t *testing.T) {
actual := corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceMemory: resource.MustParse("50Mi"),
},
}
expected := actual
got := compareContainerResources(expected, actual)
assert.True(t, got)
})
t.Run("limit CPU the same values", func(t *testing.T) {
actual := corev1.ResourceRequirements{
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("50m"),
},
}
expected := actual
got := compareContainerResources(expected, actual)
assert.True(t, got)
})
t.Run("limit memory the same values", func(t *testing.T) {
actual := corev1.ResourceRequirements{
Limits: corev1.ResourceList{
corev1.ResourceMemory: resource.MustParse("50Mi"),
},
}
expected := actual
got := compareContainerResources(expected, actual)
assert.True(t, got)
})
t.Run("request CPU different values", func(t *testing.T) {
expected := corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("5m"),
},
}
actual := corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("50m"),
},
}
got := compareContainerResources(expected, actual)
assert.False(t, got)
})
t.Run("request memory different values", func(t *testing.T) {
expected := corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceMemory: resource.MustParse("5Mi"),
},
}
actual := corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceMemory: resource.MustParse("50Mi"),
},
}
got := compareContainerResources(expected, actual)
assert.False(t, got)
})
t.Run("limit CPU different values", func(t *testing.T) {
expected := corev1.ResourceRequirements{
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("5m"),
},
}
actual := corev1.ResourceRequirements{
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("50m"),
},
}
got := compareContainerResources(expected, actual)
assert.False(t, got)
})
t.Run("limit memory different values", func(t *testing.T) {
expected := corev1.ResourceRequirements{
Limits: corev1.ResourceList{
corev1.ResourceMemory: resource.MustParse("5Mi"),
},
}
actual := corev1.ResourceRequirements{
Limits: corev1.ResourceList{
corev1.ResourceMemory: resource.MustParse("50Mi"),
},
}
got := compareContainerResources(expected, actual)
assert.False(t, got)
})
t.Run("request CPU not set in actual", func(t *testing.T) {
expected := corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("5m"),
},
}
var actual corev1.ResourceRequirements
got := compareContainerResources(expected, actual)
assert.False(t, got)
})
t.Run("request memory not set in actual", func(t *testing.T) {
expected := corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceMemory: resource.MustParse("5Mi"),
},
}
var actual corev1.ResourceRequirements
got := compareContainerResources(expected, actual)
assert.False(t, got)
})
t.Run("limit CPU not set in actual", func(t *testing.T) {
expected := corev1.ResourceRequirements{
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("5m"),
},
}
var actual corev1.ResourceRequirements
got := compareContainerResources(expected, actual)
assert.False(t, got)
})
t.Run("limit memory not set in actual", func(t *testing.T) {
expected := corev1.ResourceRequirements{
Limits: corev1.ResourceList{
corev1.ResourceMemory: resource.MustParse("5Mi"),
},
}
var actual corev1.ResourceRequirements
got := compareContainerResources(expected, actual)
assert.False(t, got)
})
}
@@ -1,213 +0,0 @@
package resources
import (
"fmt"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
basicSettingsGroovyScriptName = "1-basic-settings.groovy"
enableCSRFGroovyScriptName = "2-enable-csrf.groovy"
disableUsageStatsGroovyScriptName = "3-disable-usage-stats.groovy"
enableMasterAccessControlGroovyScriptName = "4-enable-master-access-control.groovy"
disableInsecureFeaturesGroovyScriptName = "5-disable-insecure-features.groovy"
configureKubernetesPluginGroovyScriptName = "6-configure-kubernetes-plugin.groovy"
configureViewsGroovyScriptName = "7-configure-views.groovy"
disableJobDslScriptApprovalGroovyScriptName = "8-disable-job-dsl-script-approval.groovy"
)
const basicSettingsFmt = `
import jenkins.model.Jenkins
import jenkins.model.JenkinsLocationConfiguration
import hudson.model.Node.Mode
def jenkins = Jenkins.instance
//Number of jobs that run simultaneously on master.
jenkins.setNumExecutors(%d)
//Jobs must specify that they want to run on master
jenkins.setMode(Mode.EXCLUSIVE)
jenkins.save()
`
const enableCSRF = `
import hudson.security.csrf.DefaultCrumbIssuer
import jenkins.model.Jenkins
def jenkins = Jenkins.instance
if (jenkins.getCrumbIssuer() == null) {
jenkins.setCrumbIssuer(new DefaultCrumbIssuer(true))
jenkins.save()
println('CSRF Protection enabled.')
} else {
println('CSRF Protection already configured.')
}
`
const disableUsageStats = `
import jenkins.model.Jenkins
def jenkins = Jenkins.instance
if (jenkins.isUsageStatisticsCollected()) {
jenkins.setNoUsageStatistics(true)
jenkins.save()
println('Jenkins usage stats submitting disabled.')
} else {
println('Nothing changed. Usage stats are not submitted to the Jenkins project.')
}
`
const enableMasterAccessControl = `
import jenkins.security.s2m.AdminWhitelistRule
import jenkins.model.Jenkins
// see https://wiki.jenkins-ci.org/display/JENKINS/Slave+To+Master+Access+Control
def jenkins = Jenkins.instance
jenkins.getInjector()
.getInstance(AdminWhitelistRule.class)
.setMasterKillSwitch(false) // for real though, false equals enabled..........
jenkins.save()
`
const disableInsecureFeatures = `
import jenkins.*
import jenkins.model.*
import hudson.model.*
import jenkins.security.s2m.*
def jenkins = Jenkins.instance
println("Disabling insecure Jenkins features...")
println("Disabling insecure protocols...")
println("Old protocols: [" + jenkins.getAgentProtocols().join(", ") + "]")
HashSet<String> newProtocols = new HashSet<>(jenkins.getAgentProtocols())
newProtocols.removeAll(Arrays.asList("JNLP3-connect", "JNLP2-connect", "JNLP-connect", "CLI-connect"))
println("New protocols: [" + newProtocols.join(", ") + "]")
jenkins.setAgentProtocols(newProtocols)
println("Disabling CLI access of /cli URL...")
def remove = { list ->
list.each { item ->
if (item.getClass().name.contains("CLIAction")) {
println("Removing extension ${item.getClass().name}")
list.remove(item)
}
}
}
remove(jenkins.getExtensionList(RootAction.class))
remove(jenkins.actions)
if (jenkins.getDescriptor("jenkins.CLI") != null) {
jenkins.getDescriptor("jenkins.CLI").get().setEnabled(false)
}
jenkins.save()
`
const configureKubernetesPluginFmt = `
import com.cloudbees.plugins.credentials.CredentialsScope
import com.cloudbees.plugins.credentials.SystemCredentialsProvider
import com.cloudbees.plugins.credentials.domains.Domain
import jenkins.model.Jenkins
import org.csanchez.jenkins.plugins.kubernetes.KubernetesCloud
def jenkins = Jenkins.getInstance()
def kubernetes = Jenkins.instance.clouds.getByName("kubernetes")
def add = false
if (kubernetes == null) {
add = true
kubernetes = new KubernetesCloud("kubernetes")
}
kubernetes.setServerUrl("https://kubernetes.default.svc.cluster.local:443")
kubernetes.setNamespace("%s")
kubernetes.setJenkinsUrl("%s")
kubernetes.setJenkinsTunnel("%s")
kubernetes.setRetentionTimeout(15)
if (add) {
jenkins.clouds.add(kubernetes)
}
jenkins.save()
`
const configureViews = `
import hudson.model.ListView
import jenkins.model.Jenkins
def Jenkins jenkins = Jenkins.getInstance()
def seedViewName = 'seed-jobs'
def nonSeedViewName = 'non-seed-jobs'
if (jenkins.getView(seedViewName) == null) {
def seedView = new ListView(seedViewName)
seedView.setIncludeRegex('.*` + constants.SeedJobSuffix + `.*')
jenkins.addView(seedView)
}
if (jenkins.getView(nonSeedViewName) == null) {
def nonSeedView = new ListView(nonSeedViewName)
nonSeedView.setIncludeRegex('((?!seed)(?!jenkins).)*')
jenkins.addView(nonSeedView)
}
jenkins.save()
`
const disableJobDSLScriptApproval = `
import jenkins.model.Jenkins
import javaposse.jobdsl.plugin.GlobalJobDslSecurityConfiguration
import jenkins.model.GlobalConfiguration
// disable Job DSL script approval
GlobalConfiguration.all().get(GlobalJobDslSecurityConfiguration.class).useScriptSecurity=false
GlobalConfiguration.all().get(GlobalJobDslSecurityConfiguration.class).save()
`
// GetBaseConfigurationConfigMapName returns name of Kubernetes config map used to base configuration.
func GetBaseConfigurationConfigMapName(jenkins *v1alpha2.Jenkins) string {
return fmt.Sprintf("%s-base-configuration-%s", constants.OperatorName, jenkins.ObjectMeta.Name)
}
// NewBaseConfigurationConfigMap builds Kubernetes config map used to base configuration.
func NewBaseConfigurationConfigMap(meta metav1.ObjectMeta, jenkins *v1alpha2.Jenkins) (*corev1.ConfigMap, error) {
meta.Name = GetBaseConfigurationConfigMapName(jenkins)
jenkinsServiceFQDN, err := GetJenkinsHTTPServiceFQDN(jenkins)
if err != nil {
return nil, err
}
jenkinsSlavesServiceFQDN, err := GetJenkinsSlavesServiceFQDN(jenkins)
if err != nil {
return nil, err
}
groovyScriptsMap := map[string]string{
basicSettingsGroovyScriptName: fmt.Sprintf(basicSettingsFmt, constants.DefaultAmountOfExecutors),
enableCSRFGroovyScriptName: enableCSRF,
disableUsageStatsGroovyScriptName: disableUsageStats,
enableMasterAccessControlGroovyScriptName: enableMasterAccessControl,
disableInsecureFeaturesGroovyScriptName: disableInsecureFeatures,
configureKubernetesPluginGroovyScriptName: fmt.Sprintf(configureKubernetesPluginFmt,
jenkins.ObjectMeta.Namespace,
fmt.Sprintf("http://%s:%d", jenkinsServiceFQDN, jenkins.Spec.Service.Port),
fmt.Sprintf("%s:%d", jenkinsSlavesServiceFQDN, jenkins.Spec.SlaveService.Port),
),
configureViewsGroovyScriptName: configureViews,
disableJobDslScriptApprovalGroovyScriptName: disableJobDSLScriptApproval,
}
if jenkins.Spec.Master.DisableCSRFProtection {
delete(groovyScriptsMap, enableCSRFGroovyScriptName)
}
return &corev1.ConfigMap{
TypeMeta: buildConfigMapTypeMeta(),
ObjectMeta: meta,
Data: groovyScriptsMap,
}, nil
}
@@ -1,50 +0,0 @@
package resources
import (
"fmt"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/pointer"
)
// NewJenkinsMasterPod builds Jenkins Master Kubernetes Pod resource.
func NewJenkinsDeployment(objectMeta metav1.ObjectMeta, jenkins *v1alpha2.Jenkins) *appsv1.Deployment {
serviceAccountName := objectMeta.Name
objectMeta.Annotations = jenkins.Spec.Master.Annotations
objectMeta.Name = GetJenkinsDeploymentName(jenkins)
selector := &metav1.LabelSelector{MatchLabels: objectMeta.Labels}
return &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: objectMeta.Name,
Namespace: objectMeta.Namespace,
Labels: objectMeta.Labels,
},
Spec: appsv1.DeploymentSpec{
Replicas: pointer.Int32Ptr(1),
Strategy: appsv1.DeploymentStrategy{Type: appsv1.RollingUpdateDeploymentStrategyType},
Template: corev1.PodTemplateSpec{
ObjectMeta: objectMeta,
Spec: corev1.PodSpec{
ServiceAccountName: serviceAccountName,
NodeSelector: jenkins.Spec.Master.NodeSelector,
Containers: newContainers(jenkins),
Volumes: append(GetJenkinsMasterPodBaseVolumes(jenkins), jenkins.Spec.Master.Volumes...),
SecurityContext: jenkins.Spec.Master.SecurityContext,
ImagePullSecrets: jenkins.Spec.Master.ImagePullSecrets,
Tolerations: jenkins.Spec.Master.Tolerations,
PriorityClassName: jenkins.Spec.Master.PriorityClassName,
},
},
Selector: selector,
},
}
}
// GetJenkinsMasterPodName returns Jenkins pod name for given CR
func GetJenkinsDeploymentName(jenkins *v1alpha2.Jenkins) string {
return fmt.Sprintf("jenkins-%s", jenkins.Name)
}
@@ -1,2 +0,0 @@
// Package resources contains Kubernetes resources required by Jenkins
package resources
@@ -1,85 +0,0 @@
package resources
import (
"fmt"
"text/template"
"github.com/jenkinsci/kubernetes-operator/internal/render"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const createOperatorUserFileName = "createOperatorUser.groovy"
var createOperatorUserGroovyFmtTemplate = template.Must(template.New(createOperatorUserFileName).Parse(`
import hudson.security.*
{{- if .Enable }}
def jenkins = jenkins.model.Jenkins.getInstance()
def operatorUserCreatedFile = new File('{{ .OperatorUserCreatedFilePath }}')
if (!operatorUserCreatedFile.exists()) {
def hudsonRealm = new HudsonPrivateSecurityRealm(false)
hudsonRealm.createAccount(
new File('{{ .OperatorCredentialsPath }}/{{ .OperatorUserNameFile }}').text,
new File('{{ .OperatorCredentialsPath }}/{{ .OperatorPasswordFile }}').text)
jenkins.setSecurityRealm(hudsonRealm)
def strategy = new FullControlOnceLoggedInAuthorizationStrategy()
strategy.setAllowAnonymousRead(false)
jenkins.setAuthorizationStrategy(strategy)
jenkins.save()
operatorUserCreatedFile.createNewFile()
}
{{- end }}
`))
func buildCreateJenkinsOperatorUserGroovyScript(jenkins *v1alpha2.Jenkins) (*string, error) {
data := struct {
Enable bool
OperatorCredentialsPath string
OperatorUserNameFile string
OperatorPasswordFile string
OperatorUserCreatedFilePath string
}{
Enable: jenkins.Spec.JenkinsAPISettings.AuthorizationStrategy == v1alpha2.CreateUserAuthorizationStrategy,
OperatorCredentialsPath: jenkinsOperatorCredentialsVolumePath,
OperatorUserNameFile: OperatorCredentialsSecretUserNameKey,
OperatorPasswordFile: OperatorCredentialsSecretPasswordKey,
OperatorUserCreatedFilePath: getJenkinsHomePath(jenkins) + "/operatorUserCreated",
}
output, err := render.Render(createOperatorUserGroovyFmtTemplate, data)
if err != nil {
return nil, err
}
return &output, nil
}
// GetInitConfigurationConfigMapName returns name of Kubernetes config map used to init configuration
func GetInitConfigurationConfigMapName(jenkins *v1alpha2.Jenkins) string {
return fmt.Sprintf("%s-init-configuration-%s", constants.OperatorName, jenkins.ObjectMeta.Name)
}
// NewInitConfigurationConfigMap builds Kubernetes config map used to init configuration
func NewInitConfigurationConfigMap(meta metav1.ObjectMeta, jenkins *v1alpha2.Jenkins) (*corev1.ConfigMap, error) {
meta.Name = GetInitConfigurationConfigMapName(jenkins)
createJenkinsOperatorUserGroovy, err := buildCreateJenkinsOperatorUserGroovyScript(jenkins)
if err != nil {
return nil, err
}
return &corev1.ConfigMap{
TypeMeta: buildConfigMapTypeMeta(),
ObjectMeta: meta,
Data: map[string]string{
createOperatorUserFileName: *createJenkinsOperatorUserGroovy,
},
}, nil
}
@@ -1,54 +0,0 @@
package resources
import (
"fmt"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// NewResourceObjectMeta builds ObjectMeta for all Kubernetes resources created by operator
func NewResourceObjectMeta(jenkins *v1alpha2.Jenkins) metav1.ObjectMeta {
return metav1.ObjectMeta{
Name: GetResourceName(jenkins),
Namespace: jenkins.ObjectMeta.Namespace,
Labels: BuildResourceLabels(jenkins),
}
}
// BuildResourceLabels returns labels for all Kubernetes resources created by operator
func BuildResourceLabels(jenkins *v1alpha2.Jenkins) map[string]string {
return map[string]string{
constants.LabelAppKey: constants.LabelAppValue,
constants.LabelJenkinsCRKey: jenkins.Name,
}
}
// BuildLabelsForWatchedResources returns labels for Kubernetes resources which operator want to watch
// resources with that labels should not be deleted after Jenkins CR deletion, to prevent this situation don't set
// any owner
func BuildLabelsForWatchedResources(jenkins v1alpha2.Jenkins) map[string]string {
return map[string]string{
constants.LabelAppKey: constants.LabelAppValue,
constants.LabelJenkinsCRKey: jenkins.Name,
constants.LabelWatchKey: constants.LabelWatchValue,
}
}
// GetResourceName returns name of Kubernetes resource base on Jenkins CR
func GetResourceName(jenkins *v1alpha2.Jenkins) string {
return fmt.Sprintf("%s-%s", constants.LabelAppValue, jenkins.ObjectMeta.Name)
}
// VerifyIfLabelsAreSet check is selected labels are set for specific resource
func VerifyIfLabelsAreSet(object metav1.Object, requiredLabels map[string]string) bool {
for key, value := range requiredLabels {
if object.GetLabels()[key] != value {
return false
}
}
return true
}
@@ -1,51 +0,0 @@
package resources
import (
"fmt"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
// OperatorUserName defines username for Jenkins API calls
OperatorUserName = "jenkins-operator"
// OperatorCredentialsSecretUserNameKey defines key of username in operator credentials secret
OperatorCredentialsSecretUserNameKey = "user"
// OperatorCredentialsSecretPasswordKey defines key of password in operator credentials secret
OperatorCredentialsSecretPasswordKey = "password"
// OperatorCredentialsSecretTokenKey defines key of token in operator credentials secret
OperatorCredentialsSecretTokenKey = "token"
// OperatorCredentialsSecretTokenCreationKey defines key of token creation time in operator credentials secret
OperatorCredentialsSecretTokenCreationKey = "tokenCreationTime"
)
func buildSecretTypeMeta() metav1.TypeMeta {
return metav1.TypeMeta{
Kind: "Secret",
APIVersion: "v1",
}
}
// GetOperatorCredentialsSecretName returns name of Kubernetes secret used to store jenkins operator credentials
// to allow calls to Jenkins API
func GetOperatorCredentialsSecretName(jenkins *v1alpha2.Jenkins) string {
return fmt.Sprintf("%s-credentials-%s", constants.OperatorName, jenkins.Name)
}
// NewOperatorCredentialsSecret builds the Kubernetes secret used to store jenkins operator credentials
// to allow calls to Jenkins API
func NewOperatorCredentialsSecret(meta metav1.ObjectMeta, jenkins *v1alpha2.Jenkins) *corev1.Secret {
meta.Name = GetOperatorCredentialsSecretName(jenkins)
return &corev1.Secret{
TypeMeta: buildSecretTypeMeta(),
ObjectMeta: meta,
Data: map[string][]byte{
OperatorCredentialsSecretUserNameKey: []byte(OperatorUserName),
OperatorCredentialsSecretPasswordKey: []byte(randomString(20)),
},
}
}
@@ -1,334 +0,0 @@
package resources
import (
"fmt"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
// JenkinsMasterContainerName is the Jenkins master container name in pod
JenkinsMasterContainerName = "jenkins-master"
// JenkinsHomeVolumeName is the Jenkins home volume name
JenkinsHomeVolumeName = "jenkins-home"
jenkinsPath = "/var/jenkins"
jenkinsScriptsVolumeName = "scripts"
// JenkinsScriptsVolumePath is a path where are scripts used to configure Jenkins
JenkinsScriptsVolumePath = jenkinsPath + "/scripts"
// InitScriptName is the init script name which configures init.groovy.d, scripts and install plugins
InitScriptName = "init.sh"
jenkinsOperatorCredentialsVolumeName = "operator-credentials"
jenkinsOperatorCredentialsVolumePath = jenkinsPath + "/operator-credentials"
jenkinsInitConfigurationVolumeName = "init-configuration"
jenkinsInitConfigurationVolumePath = jenkinsPath + "/init-configuration"
// GroovyScriptsSecretVolumePath is a path where are groovy scripts used to configure Jenkins
// This script is provided by user
GroovyScriptsSecretVolumePath = jenkinsPath + "/groovy-scripts-secrets"
// ConfigurationAsCodeSecretVolumePath is a path where are CasC configs used to configure Jenkins
// This script is provided by user
ConfigurationAsCodeSecretVolumePath = jenkinsPath + "/configuration-as-code-secrets"
httpPortName = "http"
slavePortName = "slavelistener"
)
func buildPodTypeMeta() metav1.TypeMeta {
return metav1.TypeMeta{
Kind: "Pod",
APIVersion: "v1",
}
}
// GetJenkinsMasterContainerBaseCommand returns default Jenkins master container command
func GetJenkinsMasterContainerBaseCommand() []string {
return []string{
"bash",
"-c",
fmt.Sprintf("%s/%s && exec /sbin/tini -s -- /usr/local/bin/jenkins.sh",
JenkinsScriptsVolumePath, InitScriptName),
}
}
// GetJenkinsMasterContainerBaseEnvs returns Jenkins master pod envs required by operator
func GetJenkinsMasterContainerBaseEnvs(jenkins *v1alpha2.Jenkins) []corev1.EnvVar {
envVars := []corev1.EnvVar{
{
Name: "COPY_REFERENCE_FILE_LOG",
Value: fmt.Sprintf("%s/%s", getJenkinsHomePath(jenkins), "copy_reference_file.log"),
},
}
if len(jenkins.Spec.ConfigurationAsCode.Secret.Name) > 0 {
envVars = append(envVars, corev1.EnvVar{
Name: "SECRETS", // https://github.com/jenkinsci/configuration-as-code-plugin/blob/master/demos/kubernetes-secrets/README.md
Value: ConfigurationAsCodeSecretVolumePath,
})
}
return envVars
}
// getJenkinsHomePath fetches the Home Path for Jenkins
func getJenkinsHomePath(jenkins *v1alpha2.Jenkins) string {
defaultJenkinsHomePath := "/var/lib/jenkins"
for _, envVar := range jenkins.Spec.Master.Containers[0].Env {
if envVar.Name == "JENKINS_HOME" {
return envVar.Value
}
}
return defaultJenkinsHomePath
}
// GetJenkinsMasterPodBaseVolumes returns Jenkins master pod volumes required by operator
func GetJenkinsMasterPodBaseVolumes(jenkins *v1alpha2.Jenkins) []corev1.Volume {
configMapVolumeSourceDefaultMode := corev1.ConfigMapVolumeSourceDefaultMode
secretVolumeSourceDefaultMode := corev1.SecretVolumeSourceDefaultMode
var scriptsVolumeDefaultMode int32 = 0777
volumes := []corev1.Volume{
{
Name: JenkinsHomeVolumeName,
VolumeSource: corev1.VolumeSource{
EmptyDir: &corev1.EmptyDirVolumeSource{},
},
},
{
Name: jenkinsScriptsVolumeName,
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
DefaultMode: &scriptsVolumeDefaultMode,
LocalObjectReference: corev1.LocalObjectReference{
Name: getScriptsConfigMapName(jenkins),
},
},
},
},
{
Name: jenkinsInitConfigurationVolumeName,
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
DefaultMode: &configMapVolumeSourceDefaultMode,
LocalObjectReference: corev1.LocalObjectReference{
Name: GetInitConfigurationConfigMapName(jenkins),
},
},
},
},
{
Name: jenkinsOperatorCredentialsVolumeName,
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
DefaultMode: &secretVolumeSourceDefaultMode,
SecretName: GetOperatorCredentialsSecretName(jenkins),
},
},
},
}
if len(jenkins.Spec.GroovyScripts.Secret.Name) > 0 {
volumes = append(volumes, corev1.Volume{
Name: getGroovyScriptsSecretVolumeName(jenkins),
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
DefaultMode: &secretVolumeSourceDefaultMode,
SecretName: jenkins.Spec.GroovyScripts.Secret.Name,
},
},
})
}
if len(jenkins.Spec.ConfigurationAsCode.Secret.Name) > 0 {
volumes = append(volumes, corev1.Volume{
Name: getConfigurationAsCodeSecretVolumeName(jenkins),
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
DefaultMode: &secretVolumeSourceDefaultMode,
SecretName: jenkins.Spec.ConfigurationAsCode.Secret.Name,
},
},
})
}
return volumes
}
func getGroovyScriptsSecretVolumeName(jenkins *v1alpha2.Jenkins) string {
return "gs-" + jenkins.Spec.GroovyScripts.Secret.Name
}
func getConfigurationAsCodeSecretVolumeName(jenkins *v1alpha2.Jenkins) string {
return "casc-" + jenkins.Spec.ConfigurationAsCode.Secret.Name
}
// GetJenkinsMasterContainerBaseVolumeMounts returns Jenkins master pod volume mounts required by operator
func GetJenkinsMasterContainerBaseVolumeMounts(jenkins *v1alpha2.Jenkins) []corev1.VolumeMount {
volumeMounts := []corev1.VolumeMount{
{
Name: JenkinsHomeVolumeName,
MountPath: getJenkinsHomePath(jenkins),
ReadOnly: false,
},
{
Name: jenkinsScriptsVolumeName,
MountPath: JenkinsScriptsVolumePath,
ReadOnly: true,
},
{
Name: jenkinsInitConfigurationVolumeName,
MountPath: jenkinsInitConfigurationVolumePath,
ReadOnly: true,
},
{
Name: jenkinsOperatorCredentialsVolumeName,
MountPath: jenkinsOperatorCredentialsVolumePath,
ReadOnly: true,
},
}
if len(jenkins.Spec.GroovyScripts.Secret.Name) > 0 {
volumeMounts = append(volumeMounts, corev1.VolumeMount{
Name: getGroovyScriptsSecretVolumeName(jenkins),
MountPath: GroovyScriptsSecretVolumePath,
ReadOnly: true,
})
}
if len(jenkins.Spec.ConfigurationAsCode.Secret.Name) > 0 {
volumeMounts = append(volumeMounts, corev1.VolumeMount{
Name: getConfigurationAsCodeSecretVolumeName(jenkins),
MountPath: ConfigurationAsCodeSecretVolumePath,
ReadOnly: true,
})
}
return volumeMounts
}
// NewJenkinsMasterContainer returns Jenkins master Kubernetes container
func NewJenkinsMasterContainer(jenkins *v1alpha2.Jenkins) corev1.Container {
jenkinsContainer := jenkins.Spec.Master.Containers[0]
envs := GetJenkinsMasterContainerBaseEnvs(jenkins)
envs = append(envs, jenkinsContainer.Env...)
jenkinsHomeEnvVar := corev1.EnvVar{
Name: "JENKINS_HOME",
Value: getJenkinsHomePath(jenkins),
}
jenkinsHomeEnvVarExists := false
for _, env := range jenkinsContainer.Env {
if env.Name == jenkinsHomeEnvVar.Name {
jenkinsHomeEnvVarExists = true
break
}
}
if !jenkinsHomeEnvVarExists {
envs = append(envs, jenkinsHomeEnvVar)
}
return corev1.Container{
Name: JenkinsMasterContainerName,
Image: jenkinsContainer.Image,
ImagePullPolicy: jenkinsContainer.ImagePullPolicy,
Command: jenkinsContainer.Command,
LivenessProbe: jenkinsContainer.LivenessProbe,
ReadinessProbe: jenkinsContainer.ReadinessProbe,
Ports: []corev1.ContainerPort{
{
Name: httpPortName,
ContainerPort: constants.DefaultHTTPPortInt32,
Protocol: corev1.ProtocolTCP,
},
{
Name: slavePortName,
ContainerPort: constants.DefaultSlavePortInt32,
Protocol: corev1.ProtocolTCP,
},
},
SecurityContext: jenkinsContainer.SecurityContext,
Env: envs,
Resources: jenkinsContainer.Resources,
VolumeMounts: append(GetJenkinsMasterContainerBaseVolumeMounts(jenkins), jenkinsContainer.VolumeMounts...),
}
}
// ConvertJenkinsContainerToKubernetesContainer converts Jenkins container to Kubernetes container
func ConvertJenkinsContainerToKubernetesContainer(container v1alpha2.Container) corev1.Container {
return corev1.Container{
Name: container.Name,
Image: container.Image,
Command: container.Command,
Args: container.Args,
WorkingDir: container.WorkingDir,
Ports: container.Ports,
EnvFrom: container.EnvFrom,
Env: container.Env,
Resources: container.Resources,
VolumeMounts: container.VolumeMounts,
LivenessProbe: container.LivenessProbe,
ReadinessProbe: container.ReadinessProbe,
Lifecycle: container.Lifecycle,
ImagePullPolicy: container.ImagePullPolicy,
SecurityContext: container.SecurityContext,
}
}
func newContainers(jenkins *v1alpha2.Jenkins) (containers []corev1.Container) {
containers = append(containers, NewJenkinsMasterContainer(jenkins))
for _, container := range jenkins.Spec.Master.Containers[1:] {
containers = append(containers, ConvertJenkinsContainerToKubernetesContainer(container))
}
return
}
// GetJenkinsMasterPodName returns Jenkins pod name for given CR
func GetJenkinsMasterPodName(jenkins *v1alpha2.Jenkins) string {
return fmt.Sprintf("jenkins-%s", jenkins.Name)
}
// GetJenkinsMasterPodLabels returns Jenkins pod labels for given CR
func GetJenkinsMasterPodLabels(jenkins v1alpha2.Jenkins) map[string]string {
var labels map[string]string
if jenkins.Spec.Master.Labels == nil {
labels = map[string]string{}
} else {
labels = jenkins.Spec.Master.Labels
}
for key, value := range BuildResourceLabels(&jenkins) {
labels[key] = value
}
return labels
}
// NewJenkinsMasterPod builds Jenkins Master Kubernetes Pod resource
func NewJenkinsMasterPod(objectMeta metav1.ObjectMeta, jenkins *v1alpha2.Jenkins) *corev1.Pod {
serviceAccountName := objectMeta.Name
objectMeta.Annotations = jenkins.Spec.Master.Annotations
objectMeta.Name = GetJenkinsMasterPodName(jenkins)
objectMeta.Labels = GetJenkinsMasterPodLabels(*jenkins)
return &corev1.Pod{
TypeMeta: buildPodTypeMeta(),
ObjectMeta: objectMeta,
Spec: corev1.PodSpec{
ServiceAccountName: serviceAccountName,
RestartPolicy: corev1.RestartPolicyNever,
NodeSelector: jenkins.Spec.Master.NodeSelector,
Containers: newContainers(jenkins),
Volumes: append(GetJenkinsMasterPodBaseVolumes(jenkins), jenkins.Spec.Master.Volumes...),
SecurityContext: jenkins.Spec.Master.SecurityContext,
ImagePullSecrets: jenkins.Spec.Master.ImagePullSecrets,
Tolerations: jenkins.Spec.Master.Tolerations,
PriorityClassName: jenkins.Spec.Master.PriorityClassName,
},
}
}
@@ -1,157 +0,0 @@
package resources
import (
"testing"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
"github.com/stretchr/testify/assert"
)
func TestGetJenkinsMasterPodBaseVolumes(t *testing.T) {
t.Run("casc and groovy script with different configMap names", func(t *testing.T) {
configMapName := "config-map"
jenkins := &v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
ConfigurationAsCode: v1alpha2.ConfigurationAsCode{
Customization: v1alpha2.Customization{
Configurations: []v1alpha2.ConfigMapRef{
{
Name: configMapName,
},
},
Secret: v1alpha2.SecretRef{
Name: "casc-script",
},
},
},
GroovyScripts: v1alpha2.GroovyScripts{
Customization: v1alpha2.Customization{
Configurations: []v1alpha2.ConfigMapRef{
{
Name: configMapName,
},
},
Secret: v1alpha2.SecretRef{
Name: "groovy-script",
},
},
},
},
}
groovyExists, cascExists := checkSecretVolumesPresence(jenkins)
assert.True(t, groovyExists)
assert.True(t, cascExists)
})
t.Run("groovy script without secret name", func(t *testing.T) {
jenkins := &v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
ConfigurationAsCode: v1alpha2.ConfigurationAsCode{
Customization: v1alpha2.Customization{
Configurations: []v1alpha2.ConfigMapRef{
{
Name: "casc-scripts",
},
},
Secret: v1alpha2.SecretRef{
Name: "jenkins-secret",
},
},
},
GroovyScripts: v1alpha2.GroovyScripts{
Customization: v1alpha2.Customization{
Configurations: []v1alpha2.ConfigMapRef{
{
Name: "groovy-scripts",
},
},
},
},
},
}
groovyExists, cascExists := checkSecretVolumesPresence(jenkins)
assert.True(t, cascExists)
assert.False(t, groovyExists)
})
t.Run("casc without secret name", func(t *testing.T) {
jenkins := &v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
ConfigurationAsCode: v1alpha2.ConfigurationAsCode{
Customization: v1alpha2.Customization{
Configurations: []v1alpha2.ConfigMapRef{
{
Name: "casc-scripts",
},
},
},
},
GroovyScripts: v1alpha2.GroovyScripts{
Customization: v1alpha2.Customization{
Configurations: []v1alpha2.ConfigMapRef{
{
Name: "groovy-scripts",
},
},
Secret: v1alpha2.SecretRef{
Name: "jenkins-secret",
},
},
},
},
}
groovyExists, cascExists := checkSecretVolumesPresence(jenkins)
assert.True(t, groovyExists)
assert.False(t, cascExists)
})
t.Run("casc and groovy script shared secret name", func(t *testing.T) {
jenkins := &v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
ConfigurationAsCode: v1alpha2.ConfigurationAsCode{
Customization: v1alpha2.Customization{
Configurations: []v1alpha2.ConfigMapRef{
{
Name: "casc-scripts",
},
},
Secret: v1alpha2.SecretRef{
Name: "jenkins-secret",
},
},
},
GroovyScripts: v1alpha2.GroovyScripts{
Customization: v1alpha2.Customization{
Configurations: []v1alpha2.ConfigMapRef{
{
Name: "groovy-scripts",
},
},
Secret: v1alpha2.SecretRef{
Name: "jenkins-secret",
},
},
},
},
}
groovyExists, cascExists := checkSecretVolumesPresence(jenkins)
assert.True(t, groovyExists)
assert.True(t, cascExists)
})
}
func checkSecretVolumesPresence(jenkins *v1alpha2.Jenkins) (groovyExists bool, cascExists bool) {
for _, volume := range GetJenkinsMasterPodBaseVolumes(jenkins) {
if volume.Name == ("gs-" + jenkins.Spec.GroovyScripts.Secret.Name) {
groovyExists = true
} else if volume.Name == ("casc-" + jenkins.Spec.ConfigurationAsCode.Secret.Name) {
cascExists = true
}
}
return groovyExists, cascExists
}
@@ -1,20 +0,0 @@
package resources
import (
"math/rand"
"time"
)
var randomCharset = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
func randomString(n int) string {
b := make([]rune, n)
for i := range b {
b[i] = randomCharset[rand.Intn(len(randomCharset))]
}
return string(b)
}
func init() {
rand.Seed(time.Now().UnixNano())
}
@@ -1,95 +0,0 @@
package resources
import (
v1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
createVerb = "create"
deleteVerb = "delete"
getVerb = "get"
listVerb = "list"
watchVerb = "watch"
patchVerb = "patch"
updateVerb = "update"
//EmptyAPIGroup short hand for the empty API group while defining policies
EmptyAPIGroup = ""
//OpenshiftAPIGroup the openshift api group name
OpenshiftAPIGroup = "image.openshift.io"
//BuildAPIGroup the openshift api group name for builds
BuildAPIGroup = "build.openshift.io"
)
// NewRole returns rbac role for jenkins master
func NewRole(meta metav1.ObjectMeta) *v1.Role {
rules := NewDefaultPolicyRules()
return &v1.Role{
TypeMeta: metav1.TypeMeta{
Kind: "Role",
APIVersion: "rbac.authorization.k8s.io/v1",
},
ObjectMeta: meta,
Rules: rules,
}
}
// NewRoleBinding returns rbac role binding for jenkins master
func NewRoleBinding(name, namespace, serviceAccountName string, roleRef v1.RoleRef) *v1.RoleBinding {
return &v1.RoleBinding{
TypeMeta: metav1.TypeMeta{
Kind: "RoleBinding",
APIVersion: "rbac.authorization.k8s.io/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
RoleRef: roleRef,
Subjects: []v1.Subject{
{
Kind: "ServiceAccount",
Name: serviceAccountName,
Namespace: namespace,
},
},
}
}
// NewDefaultPolicyRules sets the default policy rules
func NewDefaultPolicyRules() []v1.PolicyRule {
var rules []v1.PolicyRule
readOnly := []string{getVerb, listVerb, watchVerb}
Default := []string{createVerb, deleteVerb, getVerb, listVerb, patchVerb, updateVerb, watchVerb}
create := []string{createVerb}
watch := []string{watchVerb}
rules = append(rules, NewPolicyRule(EmptyAPIGroup, "pods/portforward", create))
rules = append(rules, NewPolicyRule(EmptyAPIGroup, "pods", Default))
rules = append(rules, NewPolicyRule(EmptyAPIGroup, "pods/exec", Default))
rules = append(rules, NewPolicyRule(EmptyAPIGroup, "configmaps", readOnly))
rules = append(rules, NewPolicyRule(EmptyAPIGroup, "pods/log", readOnly))
rules = append(rules, NewPolicyRule(EmptyAPIGroup, "secrets", readOnly))
rules = append(rules, NewPolicyRule(EmptyAPIGroup, "events", watch))
rules = append(rules, NewOpenShiftPolicyRule(OpenshiftAPIGroup, "imagestreams", readOnly))
rules = append(rules, NewOpenShiftPolicyRule(BuildAPIGroup, "buildconfigs", readOnly))
rules = append(rules, NewOpenShiftPolicyRule(BuildAPIGroup, "builds", readOnly))
return rules
}
// NewPolicyRule returns a policyRule allowing verbs on resources
func NewPolicyRule(apiGroup string, resource string, verbs []string) v1.PolicyRule {
rule := v1.PolicyRule{
APIGroups: []string{apiGroup},
Resources: []string{resource},
Verbs: verbs,
}
return rule
}
// NewOpenShiftPolicyRule returns a policyRule allowing verbs on resources
func NewOpenShiftPolicyRule(apiGroup string, resource string, verbs []string) v1.PolicyRule {
return NewPolicyRule(apiGroup, resource, verbs)
}
@@ -1,51 +0,0 @@
package resources
import (
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
routev1 "github.com/openshift/api/route/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/client-go/discovery"
"k8s.io/client-go/kubernetes"
)
//RouteKind the kind name for route
const RouteKind = "Route"
var isRouteAPIAvailable = false
var routeAPIChecked = false
// UpdateRoute returns new route matching the service
func UpdateRoute(actual routev1.Route, jenkins *v1alpha2.Jenkins) routev1.Route {
actualTargetService := actual.Spec.To
serviceName := GetJenkinsHTTPServiceName(jenkins)
if actualTargetService.Name != serviceName {
actual.Spec.To.Name = serviceName
}
port := jenkins.Spec.Service.Port
if actual.Spec.Port.TargetPort.IntVal != port {
actual.Spec.Port.TargetPort = intstr.FromInt(int(port))
}
return actual
}
//IsRouteAPIAvailable tells if the Route API is installed and discoverable
func IsRouteAPIAvailable(clientSet *kubernetes.Clientset) bool {
if routeAPIChecked {
return isRouteAPIAvailable
}
gv := schema.GroupVersion{
Group: routev1.GroupName,
Version: routev1.SchemeGroupVersion.Version,
}
if err := discovery.ServerSupportsVersion(clientSet, gv); err != nil {
// error, API not available
routeAPIChecked = true
isRouteAPIAvailable = false
} else {
// API Exists
routeAPIChecked = true
isRouteAPIAvailable = true
}
return isRouteAPIAvailable
}
@@ -1,349 +0,0 @@
package resources
import (
"fmt"
"text/template"
"github.com/jenkinsci/kubernetes-operator/internal/render"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const installPluginsCommand = "install-plugins.sh"
// bash scripts installs single jenkins plugin with specific version
const installPluginsBashFmt = `#!/bin/bash -eu
# Resolve dependencies and download plugins given on the command line
#
# FROM jenkins
# RUN install-plugins.sh docker-slaves github-branch-source
set -o pipefail
REF_DIR=${REF:-%s/plugins}
FAILED="$REF_DIR/failed-plugins.txt"
. /usr/local/bin/jenkins-support
getLockFile() {
printf '%%s' "$REF_DIR/${1}.lock"
}
getArchiveFilename() {
printf '%%s' "$REF_DIR/${1}.jpi"
}
download() {
local plugin originalPlugin version lock ignoreLockFile
plugin="$1"
version="${2:-latest}"
ignoreLockFile="${3:-}"
lock="$(getLockFile "$plugin")"
if [[ $ignoreLockFile ]] || mkdir "$lock" &>/dev/null; then
if ! doDownload "$plugin" "$version"; then
# some plugin don't follow the rules about artifact ID
# typically: docker-plugin
originalPlugin="$plugin"
plugin="${plugin}-plugin"
if ! doDownload "$plugin" "$version"; then
echo "Failed to download plugin: $originalPlugin or $plugin" >&2
echo "Not downloaded: ${originalPlugin}" >> "$FAILED"
return 1
fi
fi
if ! checkIntegrity "$plugin"; then
echo "Downloaded file is not a valid ZIP: $(getArchiveFilename "$plugin")" >&2
echo "Download integrity: ${plugin}" >> "$FAILED"
return 1
fi
fi
}
doDownload() {
local plugin version url jpi
plugin="$1"
version="$2"
jpi="$(getArchiveFilename "$plugin")"
# If plugin already exists and is the same version do not download
if test -f "$jpi" && unzip -p "$jpi" META-INF/MANIFEST.MF | tr -d '\r' | grep "^Plugin-Version: ${version}$" > /dev/null; then
echo "Using provided plugin: $plugin"
return 0
fi
if [[ "$version" == "latest" && -n "$JENKINS_UC_LATEST" ]]; then
# If version-specific Update Center is available, which is the case for LTS versions,
# use it to resolve latest versions.
url="$JENKINS_UC_LATEST/latest/${plugin}.hpi"
elif [[ "$version" == "experimental" && -n "$JENKINS_UC_EXPERIMENTAL" ]]; then
# Download from the experimental update center
url="$JENKINS_UC_EXPERIMENTAL/latest/${plugin}.hpi"
elif [[ "$version" == incrementals* ]] ; then
# Download from Incrementals repo: https://jenkins.io/blog/2018/05/15/incremental-deployment/
# Example URL: https://repo.jenkins-ci.org/incrementals/org/jenkins-ci/plugins/workflow/workflow-support/2.19-rc289.d09828a05a74/workflow-support-2.19-rc289.d09828a05a74.hpi
local groupId incrementalsVersion
arrIN=(${version//;/ })
groupId=${arrIN[1]}
incrementalsVersion=${arrIN[2]}
url="${JENKINS_INCREMENTALS_REPO_MIRROR}/$(echo "${groupId}" | tr '.' '/')/${plugin}/${incrementalsVersion}/${plugin}-${incrementalsVersion}.hpi"
else
JENKINS_UC_DOWNLOAD=${JENKINS_UC_DOWNLOAD:-"$JENKINS_UC/download"}
url="$JENKINS_UC_DOWNLOAD/plugins/$plugin/$version/${plugin}.hpi"
fi
echo "Downloading plugin: $plugin from $url"
retry_command curl "${CURL_OPTIONS:--sSfL}" --connect-timeout "${CURL_CONNECTION_TIMEOUT:-20}" --retry "${CURL_RETRY:-5}" --retry-delay "${CURL_RETRY_DELAY:-0}" --retry-max-time "${CURL_RETRY_MAX_TIME:-60}" "$url" -o "$jpi"
return $?
}
checkIntegrity() {
local plugin jpi
plugin="$1"
jpi="$(getArchiveFilename "$plugin")"
unzip -t -qq "$jpi" >/dev/null
return $?
}
bundledPlugins() {
local JENKINS_WAR=/usr/share/jenkins/jenkins.war
if [ -f $JENKINS_WAR ]
then
TEMP_PLUGIN_DIR=/tmp/plugintemp.$$
for i in $(jar tf $JENKINS_WAR | grep -E '[^detached-]plugins.*\..pi' | sort)
do
rm -fr $TEMP_PLUGIN_DIR
mkdir -p $TEMP_PLUGIN_DIR
PLUGIN=$(basename "$i"|cut -f1 -d'.')
(cd $TEMP_PLUGIN_DIR;jar xf "$JENKINS_WAR" "$i";jar xvf "$TEMP_PLUGIN_DIR/$i" META-INF/MANIFEST.MF >/dev/null 2>&1)
VER=$(grep -E -i Plugin-Version "$TEMP_PLUGIN_DIR/META-INF/MANIFEST.MF"|cut -d: -f2|sed 's/ //')
echo "$PLUGIN:$VER"
done
rm -fr $TEMP_PLUGIN_DIR
else
rm -f "$TEMP_ALREADY_INSTALLED"
echo "ERROR file not found: $JENKINS_WAR"
exit 1
fi
}
versionFromPlugin() {
local plugin=$1
if [[ $plugin =~ .*:.* ]]; then
echo "${plugin##*:}"
else
echo "latest"
fi
}
installedPlugins() {
for f in "$REF_DIR"/*.jpi; do
echo "$(basename "$f" | sed -e 's/\.jpi//'):$(get_plugin_version "$f")"
done
}
jenkinsMajorMinorVersion() {
local JENKINS_WAR
JENKINS_WAR=/usr/share/jenkins/jenkins.war
if [[ -f "$JENKINS_WAR" ]]; then
local version major minor
version="$(java -jar /usr/share/jenkins/jenkins.war --version)"
major="$(echo "$version" | cut -d '.' -f 1)"
minor="$(echo "$version" | cut -d '.' -f 2)"
echo "$major.$minor"
else
echo "ERROR file not found: $JENKINS_WAR"
return 1
fi
}
main() {
local plugin pluginVersion jenkinsVersion
local plugins=()
mkdir -p "$REF_DIR" || exit 1
rm -f "$FAILED"
# Read plugins from stdin or from the command line arguments
if [[ ($# -eq 0) ]]; then
while read -r line || [ "$line" != "" ]; do
# Remove leading/trailing spaces, comments, and empty lines
plugin=$(echo "${line}" | tr -d '\r' | sed -e 's/^[ \t]*//g' -e 's/[ \t]*$//g' -e 's/[ \t]*#.*$//g' -e '/^[ \t]*$/d')
# Avoid adding empty plugin into array
if [ ${#plugin} -ne 0 ]; then
plugins+=("${plugin}")
fi
done
else
plugins=("$@")
fi
# Create lockfile manually before first run to make sure any explicit version set is used.
echo "Creating initial locks..."
for plugin in "${plugins[@]}"; do
mkdir "$(getLockFile "${plugin%%:*}")"
done
echo "Analyzing war..."
bundledPlugins="$(bundledPlugins)"
echo "Registering preinstalled plugins..."
installedPlugins="$(installedPlugins)"
# Check if there's a version-specific update center, which is the case for LTS versions
jenkinsVersion="$(jenkinsMajorMinorVersion)"
if curl -fsL -o /dev/null "$JENKINS_UC/$jenkinsVersion"; then
JENKINS_UC_LATEST="$JENKINS_UC/$jenkinsVersion"
echo "Using version-specific update center: $JENKINS_UC_LATEST..."
else
JENKINS_UC_LATEST=
fi
echo "Downloading plugins..."
for plugin in "${plugins[@]}"; do
pluginVersion=""
if [[ $plugin =~ .*:.* ]]; then
pluginVersion=$(versionFromPlugin "${plugin}")
plugin="${plugin%%:*}"
fi
download "$plugin" "$pluginVersion" "true" &
done
wait
echo
echo "WAR bundled plugins:"
echo "${bundledPlugins}"
echo
echo "Installed plugins:"
installedPlugins
if [[ -f $FAILED ]]; then
echo "Some plugins failed to download!" "$(<"$FAILED")" >&2
exit 1
fi
echo "Cleaning up locks"
rm -r "$REF_DIR"/*.lock
}
main "$@"
`
var initBashTemplate = template.Must(template.New(InitScriptName).Parse(`#!/usr/bin/env bash
set -e
set -x
if [ "${DEBUG_JENKINS_OPERATOR}" == "true" ]; then
echo "Printing debug messages - begin"
id
env
ls -la {{ .JenkinsHomePath }}
echo "Printing debug messages - end"
else
echo "To print debug messages set environment variable 'DEBUG_JENKINS_OPERATOR' to 'true'"
fi
# https://wiki.jenkins.io/display/JENKINS/Post-initialization+script
mkdir -p {{ .JenkinsHomePath }}/init.groovy.d
cp -n {{ .InitConfigurationPath }}/*.groovy {{ .JenkinsHomePath }}/init.groovy.d
mkdir -p {{ .JenkinsHomePath }}/scripts
cp {{ .JenkinsScriptsVolumePath }}/*.sh {{ .JenkinsHomePath }}/scripts
chmod +x {{ .JenkinsHomePath }}/scripts/*.sh
{{- $jenkinsHomePath := .JenkinsHomePath }}
{{- $installPluginsCommand := .InstallPluginsCommand }}
echo "Installing plugins required by Operator - begin"
cat > {{ .JenkinsHomePath }}/base-plugins << EOF
{{ range $index, $plugin := .BasePlugins }}
{{ $plugin.Name }}:{{ $plugin.Version }}{{if $plugin.DownloadURL}}:{{ $plugin.DownloadURL }}{{end}}
{{ end }}
EOF
if [[ -z "${OPENSHIFT_JENKINS_IMAGE_VERSION}" ]]; then
{{ $installPluginsCommand }} < {{ .JenkinsHomePath }}/base-plugins
else
{{ $installPluginsCommand }} {{ .JenkinsHomePath }}/base-plugins
fi
echo "Installing plugins required by Operator - end"
echo "Installing plugins required by user - begin"
cat > {{ .JenkinsHomePath }}/user-plugins << EOF
{{ range $index, $plugin := .UserPlugins }}
{{ $plugin.Name }}:{{ $plugin.Version }}{{if $plugin.DownloadURL}}:{{ $plugin.DownloadURL }}{{end}}
{{ end }}
EOF
if [[ -z "${OPENSHIFT_JENKINS_IMAGE_VERSION}" ]]; then
{{ $installPluginsCommand }} < {{ .JenkinsHomePath }}/user-plugins
else
{{ $installPluginsCommand }} {{ .JenkinsHomePath }}/user-plugins
fi
echo "Installing plugins required by user - end"
`))
func buildConfigMapTypeMeta() metav1.TypeMeta {
return metav1.TypeMeta{
Kind: "ConfigMap",
APIVersion: "v1",
}
}
func buildInitBashScript(jenkins *v1alpha2.Jenkins) (*string, error) {
data := struct {
JenkinsHomePath string
InitConfigurationPath string
InstallPluginsCommand string
JenkinsScriptsVolumePath string
BasePlugins []v1alpha2.Plugin
UserPlugins []v1alpha2.Plugin
}{
JenkinsHomePath: getJenkinsHomePath(jenkins),
InitConfigurationPath: jenkinsInitConfigurationVolumePath,
BasePlugins: jenkins.Spec.Master.BasePlugins,
UserPlugins: jenkins.Spec.Master.Plugins,
InstallPluginsCommand: installPluginsCommand,
JenkinsScriptsVolumePath: JenkinsScriptsVolumePath,
}
output, err := render.Render(initBashTemplate, data)
if err != nil {
return nil, err
}
return &output, nil
}
func getScriptsConfigMapName(jenkins *v1alpha2.Jenkins) string {
return fmt.Sprintf("%s-scripts-%s", constants.OperatorName, jenkins.ObjectMeta.Name)
}
// NewScriptsConfigMap builds Kubernetes config map used to store scripts
func NewScriptsConfigMap(meta metav1.ObjectMeta, jenkins *v1alpha2.Jenkins) (*corev1.ConfigMap, error) {
meta.Name = getScriptsConfigMapName(jenkins)
initBashScript, err := buildInitBashScript(jenkins)
if err != nil {
return nil, err
}
return &corev1.ConfigMap{
TypeMeta: buildConfigMapTypeMeta(),
ObjectMeta: meta,
Data: map[string]string{
InitScriptName: *initBashScript,
installPluginsCommand: fmt.Sprintf(installPluginsBashFmt, getJenkinsHomePath(jenkins)),
},
}, nil
}
@@ -1,103 +0,0 @@
package resources
import (
"fmt"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants"
"github.com/operator-framework/operator-sdk/pkg/k8sutil"
stackerr "github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
"net"
"strings"
)
//ServiceKind the kind name for Service
const ServiceKind = "Service"
// UpdateService returns new service with override fields from config
func UpdateService(actual corev1.Service, config v1alpha2.Service) corev1.Service {
actual.ObjectMeta.Annotations = config.Annotations
for key, value := range config.Labels {
actual.ObjectMeta.Labels[key] = value
}
actual.Spec.Type = config.Type
actual.Spec.LoadBalancerIP = config.LoadBalancerIP
actual.Spec.LoadBalancerSourceRanges = config.LoadBalancerSourceRanges
if len(actual.Spec.Ports) == 0 {
actual.Spec.Ports = []corev1.ServicePort{{}}
}
actual.Spec.Ports[0].Port = config.Port
if config.NodePort != 0 {
actual.Spec.Ports[0].NodePort = config.NodePort
}
return actual
}
// GetJenkinsHTTPServiceName returns Kubernetes service name used for expose Jenkins HTTP endpoint
func GetJenkinsHTTPServiceName(jenkins *v1alpha2.Jenkins) string {
return fmt.Sprintf("%s-http-%s", constants.OperatorName, jenkins.ObjectMeta.Name)
}
// GetJenkinsSlavesServiceName returns Kubernetes service name used for expose Jenkins slave endpoint
func GetJenkinsSlavesServiceName(jenkins *v1alpha2.Jenkins) string {
return fmt.Sprintf("%s-slave-%s", constants.OperatorName, jenkins.ObjectMeta.Name)
}
// GetJenkinsHTTPServiceFQDN returns Kubernetes service FQDN used for expose Jenkins HTTP endpoint
func GetJenkinsHTTPServiceFQDN(jenkins *v1alpha2.Jenkins) (string, error) {
clusterDomain, err := getClusterDomain()
if err != nil {
return "", err
}
return fmt.Sprintf("%s-http-%s.%s.svc.%s", constants.OperatorName, jenkins.ObjectMeta.Name, jenkins.ObjectMeta.Namespace, clusterDomain), nil
}
// GetJenkinsSlavesServiceFQDN returns Kubernetes service FQDN used for expose Jenkins slave endpoint
func GetJenkinsSlavesServiceFQDN(jenkins *v1alpha2.Jenkins) (string, error) {
clusterDomain, err := getClusterDomain()
if err != nil {
return "", err
}
return fmt.Sprintf("%s-slave-%s.%s.svc.%s", constants.OperatorName, jenkins.ObjectMeta.Name, jenkins.ObjectMeta.Namespace, clusterDomain), nil
}
// GetClusterDomain returns Kubernetes cluster domain, default to "cluster.local"
func getClusterDomain() (string, error) {
clusterDomain := "cluster.local"
if ok, err := isRunningInCluster(); !ok {
return clusterDomain, nil
} else if err != nil {
return "", nil
}
apiSvc := "kubernetes.default.svc"
cname, err := net.LookupCNAME(apiSvc)
if err != nil {
return "", stackerr.WithStack(err)
}
clusterDomain = strings.TrimPrefix(cname, "kubernetes.default.svc")
clusterDomain = strings.TrimPrefix(clusterDomain, ".")
clusterDomain = strings.TrimSuffix(clusterDomain, ".")
return clusterDomain, nil
}
func isRunningInCluster() (bool, error) {
_, err := k8sutil.GetOperatorNamespace()
if err != nil {
if err == k8sutil.ErrNoNamespace || err == k8sutil.ErrRunLocal {
return false, nil
}
return true, nil
}
return false, stackerr.WithStack(err)
}
@@ -1,18 +0,0 @@
package resources
import (
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// NewServiceAccount return Kubernetes service account
func NewServiceAccount(meta metav1.ObjectMeta, annotations map[string]string) *v1.ServiceAccount {
meta.Annotations = annotations
return &v1.ServiceAccount{
TypeMeta: metav1.TypeMeta{
Kind: "ServiceAccount",
APIVersion: "v1",
},
ObjectMeta: meta,
}
}
@@ -1,58 +0,0 @@
package base
import (
"context"
"fmt"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
"k8s.io/apimachinery/pkg/util/intstr"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources"
routev1 "github.com/openshift/api/route/v1"
stackerr "github.com/pkg/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)
// createRoute takes the ServiceName and Creates the Route based on it
func (r *ReconcileJenkinsBaseConfiguration) createRoute(meta metav1.ObjectMeta, serviceName string, config *v1alpha2.Jenkins) error {
route := routev1.Route{}
name := fmt.Sprintf("%s-%s", config.ObjectMeta.Name, config.ObjectMeta.Namespace)
err := r.Client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: meta.Namespace}, &route)
if err != nil && apierrors.IsNotFound(err) {
port := &routev1.RoutePort{
TargetPort: intstr.FromString(""),
}
routeSpec := routev1.RouteSpec{
TLS: &routev1.TLSConfig{
InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect,
Termination: routev1.TLSTerminationEdge,
},
To: routev1.RouteTargetReference{
Kind: resources.ServiceKind,
Name: serviceName,
},
Port: port,
}
actual := routev1.Route{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: meta.Namespace,
Labels: meta.Labels,
},
Spec: routeSpec,
}
route = resources.UpdateRoute(actual, config)
if err = r.CreateResource(&route); err != nil {
return stackerr.WithStack(err)
}
} else if err != nil {
return stackerr.WithStack(err)
}
route.ObjectMeta.Labels = meta.Labels // make sure that user won't break service by hand
route = resources.UpdateRoute(route, config)
return stackerr.WithStack(r.UpdateResource(&route))
}
@@ -1,40 +0,0 @@
package base
import (
"context"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources"
stackerr "github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)
func (r *ReconcileJenkinsBaseConfiguration) createService(meta metav1.ObjectMeta, name string, config v1alpha2.Service) error {
service := corev1.Service{}
err := r.Client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: meta.Namespace}, &service)
if err != nil && apierrors.IsNotFound(err) {
service = resources.UpdateService(corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: meta.Namespace,
Labels: meta.Labels,
},
Spec: corev1.ServiceSpec{
Selector: meta.Labels,
},
}, config)
if err = r.CreateResource(&service); err != nil {
return stackerr.WithStack(err)
}
} else if err != nil {
return stackerr.WithStack(err)
}
service.Spec.Selector = meta.Labels // make sure that user won't break service by hand
service = resources.UpdateService(service, config)
return stackerr.WithStack(r.UpdateResource(&service))
}
@@ -1,45 +0,0 @@
package base
import (
"context"
"fmt"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources"
"github.com/jenkinsci/kubernetes-operator/pkg/log"
stackerr "github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)
func (r *ReconcileJenkinsBaseConfiguration) createServiceAccount(meta metav1.ObjectMeta) error {
serviceAccount := &corev1.ServiceAccount{}
err := r.Client.Get(context.TODO(), types.NamespacedName{Name: meta.Name, Namespace: meta.Namespace}, serviceAccount)
annotations := r.Configuration.Jenkins.Spec.ServiceAccount.Annotations
msg := fmt.Sprintf("createServiceAccount with annotations %v", annotations)
r.logger.V(log.VDebug).Info(msg)
if err != nil && apierrors.IsNotFound(err) {
serviceAccount = resources.NewServiceAccount(meta, annotations)
if err = r.CreateResource(serviceAccount); err != nil {
return stackerr.WithStack(err)
}
} else if err != nil {
return stackerr.WithStack(err)
}
if !compareMap(annotations, serviceAccount.Annotations) {
if serviceAccount.Annotations == nil {
serviceAccount.Annotations = map[string]string{}
}
for key, value := range annotations {
serviceAccount.Annotations[key] = value
}
if err = r.UpdateResource(serviceAccount); err != nil {
return stackerr.WithStack(err)
}
}
return nil
}
@@ -1,410 +0,0 @@
package base
import (
"context"
"fmt"
"regexp"
"strings"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/plugins"
docker "github.com/docker/distribution/reference"
stackerr "github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
)
var (
dockerImageRegexp = regexp.MustCompile(`^` + docker.TagRegexp.String() + `$`)
)
// Validate validates Jenkins CR Spec.master section
func (r *ReconcileJenkinsBaseConfiguration) Validate(jenkins *v1alpha2.Jenkins) ([]string, error) {
var messages []string
if msg := r.validateReservedVolumes(); len(msg) > 0 {
messages = append(messages, msg...)
}
if msg, err := r.validateVolumes(); err != nil {
return nil, err
} else if len(msg) > 0 {
messages = append(messages, msg...)
}
for _, container := range jenkins.Spec.Master.Containers {
if msg := r.validateContainer(container); len(msg) > 0 {
for _, m := range msg {
messages = append(messages, fmt.Sprintf("Container `%s` - %s", container.Name, m))
}
}
}
if msg := r.validatePlugins(plugins.BasePlugins(), jenkins.Spec.Master.BasePlugins, jenkins.Spec.Master.Plugins); len(msg) > 0 {
messages = append(messages, msg...)
}
if msg := r.validateJenkinsMasterPodEnvs(); len(msg) > 0 {
messages = append(messages, msg...)
}
if msg, err := r.validateCustomization(r.Configuration.Jenkins.Spec.GroovyScripts.Customization, "spec.groovyScripts"); err != nil {
return nil, err
} else if len(msg) > 0 {
messages = append(messages, msg...)
}
if msg, err := r.validateCustomization(r.Configuration.Jenkins.Spec.ConfigurationAsCode.Customization, "spec.configurationAsCode"); err != nil {
return nil, err
} else if len(msg) > 0 {
messages = append(messages, msg...)
}
if jenkins.Spec.JenkinsAPISettings.AuthorizationStrategy != v1alpha2.CreateUserAuthorizationStrategy && jenkins.Spec.JenkinsAPISettings.AuthorizationStrategy != v1alpha2.ServiceAccountAuthorizationStrategy {
messages = append(messages, fmt.Sprintf("unrecognized '%s' spec.jenkinsAPISettings.authorizationStrategy", jenkins.Spec.JenkinsAPISettings.AuthorizationStrategy))
}
return messages, nil
}
func (r *ReconcileJenkinsBaseConfiguration) validateJenkinsMasterContainerCommand() []string {
masterContainer := r.Configuration.GetJenkinsMasterContainer()
if masterContainer == nil {
return []string{}
}
jenkinsOperatorInitScript := fmt.Sprintf("%s/%s && ", resources.JenkinsScriptsVolumePath, resources.InitScriptName)
correctCommand := []string{
"bash",
"-c",
fmt.Sprintf("%s<optional-custom-command> && exec <command-which-start-jenkins>", jenkinsOperatorInitScript),
}
invalidCommandMessage := []string{fmt.Sprintf("spec.master.containers[%s].command is invalid, make sure it looks like '%v', otherwise the operator won't configure default user and install plugins. 'exec' is required to propagate signals to the Jenkins.", masterContainer.Name, correctCommand)}
if len(masterContainer.Command) != 3 {
return invalidCommandMessage
}
if masterContainer.Command[0] != correctCommand[0] {
return invalidCommandMessage
}
if masterContainer.Command[1] != correctCommand[1] {
return invalidCommandMessage
}
if !strings.HasPrefix(masterContainer.Command[2], jenkinsOperatorInitScript) {
return invalidCommandMessage
}
if !strings.Contains(masterContainer.Command[2], "exec") {
return invalidCommandMessage
}
return []string{}
}
func (r *ReconcileJenkinsBaseConfiguration) validateImagePullSecrets() ([]string, error) {
var messages []string
for _, sr := range r.Configuration.Jenkins.Spec.Master.ImagePullSecrets {
msg, err := r.validateImagePullSecret(sr.Name)
if err != nil {
return nil, err
}
if len(msg) > 0 {
messages = append(messages, msg...)
}
}
return messages, nil
}
func (r *ReconcileJenkinsBaseConfiguration) validateImagePullSecret(secretName string) ([]string, error) {
var messages []string
secret := &corev1.Secret{}
err := r.Client.Get(context.TODO(), types.NamespacedName{Name: secretName, Namespace: r.Configuration.Jenkins.ObjectMeta.Namespace}, secret)
if err != nil && apierrors.IsNotFound(err) {
messages = append(messages, fmt.Sprintf("Secret %s not found defined in spec.master.imagePullSecrets", secretName))
} else if err != nil && !apierrors.IsNotFound(err) {
return nil, stackerr.WithStack(err)
}
if secret.Data["docker-server"] == nil {
messages = append(messages, fmt.Sprintf("Secret '%s' defined in spec.master.imagePullSecrets doesn't have 'docker-server' key.", secretName))
}
if secret.Data["docker-username"] == nil {
messages = append(messages, fmt.Sprintf("Secret '%s' defined in spec.master.imagePullSecrets doesn't have 'docker-username' key.", secretName))
}
if secret.Data["docker-password"] == nil {
messages = append(messages, fmt.Sprintf("Secret '%s' defined in spec.master.imagePullSecrets doesn't have 'docker-password' key.", secretName))
}
if secret.Data["docker-email"] == nil {
messages = append(messages, fmt.Sprintf("Secret '%s' defined in spec.master.imagePullSecrets doesn't have 'docker-email' key.", secretName))
}
return messages, nil
}
func (r *ReconcileJenkinsBaseConfiguration) validateVolumes() ([]string, error) {
var messages []string
for _, volume := range r.Configuration.Jenkins.Spec.Master.Volumes {
switch {
case volume.ConfigMap != nil:
if msg, err := r.validateConfigMapVolume(volume); err != nil {
return nil, err
} else if len(msg) > 0 {
messages = append(messages, msg...)
}
case volume.Secret != nil:
if msg, err := r.validateSecretVolume(volume); err != nil {
return nil, err
} else if len(msg) > 0 {
messages = append(messages, msg...)
}
case volume.PersistentVolumeClaim != nil:
if msg, err := r.validatePersistentVolumeClaim(volume); err != nil {
return nil, err
} else if len(msg) > 0 {
messages = append(messages, msg...)
}
}
}
return messages, nil
}
func (r *ReconcileJenkinsBaseConfiguration) validatePersistentVolumeClaim(volume corev1.Volume) ([]string, error) {
var messages []string
pvc := &corev1.PersistentVolumeClaim{}
err := r.Client.Get(context.TODO(), types.NamespacedName{Name: volume.PersistentVolumeClaim.ClaimName, Namespace: r.Configuration.Jenkins.ObjectMeta.Namespace}, pvc)
if err != nil && apierrors.IsNotFound(err) {
messages = append(messages, fmt.Sprintf("PersistentVolumeClaim '%s' not found for volume '%v'", volume.PersistentVolumeClaim.ClaimName, volume))
} else if err != nil && !apierrors.IsNotFound(err) {
return nil, stackerr.WithStack(err)
}
return messages, nil
}
func (r *ReconcileJenkinsBaseConfiguration) validateConfigMapVolume(volume corev1.Volume) ([]string, error) {
var messages []string
if volume.ConfigMap.Optional != nil && *volume.ConfigMap.Optional {
return nil, nil
}
configMap := &corev1.ConfigMap{}
err := r.Client.Get(context.TODO(), types.NamespacedName{Name: volume.ConfigMap.Name, Namespace: r.Configuration.Jenkins.ObjectMeta.Namespace}, configMap)
if err != nil && apierrors.IsNotFound(err) {
messages = append(messages, fmt.Sprintf("ConfigMap '%s' not found for volume '%v'", volume.ConfigMap.Name, volume))
} else if err != nil && !apierrors.IsNotFound(err) {
return nil, stackerr.WithStack(err)
}
return messages, nil
}
func (r *ReconcileJenkinsBaseConfiguration) validateSecretVolume(volume corev1.Volume) ([]string, error) {
var messages []string
if volume.Secret.Optional != nil && *volume.Secret.Optional {
return nil, nil
}
secret := &corev1.Secret{}
err := r.Client.Get(context.TODO(), types.NamespacedName{Name: volume.Secret.SecretName, Namespace: r.Configuration.Jenkins.ObjectMeta.Namespace}, secret)
if err != nil && apierrors.IsNotFound(err) {
messages = append(messages, fmt.Sprintf("Secret '%s' not found for volume '%v'", volume.Secret.SecretName, volume))
} else if err != nil && !apierrors.IsNotFound(err) {
return nil, stackerr.WithStack(err)
}
return messages, nil
}
func (r *ReconcileJenkinsBaseConfiguration) validateReservedVolumes() []string {
var messages []string
for _, baseVolume := range resources.GetJenkinsMasterPodBaseVolumes(r.Configuration.Jenkins) {
for _, volume := range r.Configuration.Jenkins.Spec.Master.Volumes {
if baseVolume.Name == volume.Name {
messages = append(messages, fmt.Sprintf("Jenkins Master pod volume '%s' is reserved please choose different one", volume.Name))
}
}
}
return messages
}
func (r *ReconcileJenkinsBaseConfiguration) validateContainer(container v1alpha2.Container) []string {
var messages []string
if container.Image == "" {
messages = append(messages, "Image not set")
}
if !dockerImageRegexp.MatchString(container.Image) && !docker.ReferenceRegexp.MatchString(container.Image) {
messages = append(messages, "Invalid image")
}
if container.ImagePullPolicy == "" {
messages = append(messages, "Image pull policy not set")
}
if msg := r.validateContainerVolumeMounts(container); len(msg) > 0 {
messages = append(messages, msg...)
}
return messages
}
func (r *ReconcileJenkinsBaseConfiguration) validateContainerVolumeMounts(container v1alpha2.Container) []string {
var messages []string
allVolumes := append(resources.GetJenkinsMasterPodBaseVolumes(r.Configuration.Jenkins), r.Configuration.Jenkins.Spec.Master.Volumes...)
for _, volumeMount := range container.VolumeMounts {
if len(volumeMount.MountPath) == 0 {
messages = append(messages, fmt.Sprintf("mountPath not set for '%s' volume mount in container '%s'", volumeMount.Name, container.Name))
}
foundVolume := false
for _, volume := range allVolumes {
if volumeMount.Name == volume.Name {
foundVolume = true
}
}
if !foundVolume {
messages = append(messages, fmt.Sprintf("Not found volume for '%s' volume mount in container '%s'", volumeMount.Name, container.Name))
}
}
return messages
}
func (r *ReconcileJenkinsBaseConfiguration) validateJenkinsMasterPodEnvs() []string {
var messages []string
baseEnvs := resources.GetJenkinsMasterContainerBaseEnvs(r.Configuration.Jenkins)
baseEnvNames := map[string]string{}
for _, env := range baseEnvs {
baseEnvNames[env.Name] = env.Value
}
javaOpts := corev1.EnvVar{}
for _, userEnv := range r.Configuration.Jenkins.Spec.Master.Containers[0].Env {
if userEnv.Name == constants.JavaOpsVariableName {
javaOpts = userEnv
}
if _, overriding := baseEnvNames[userEnv.Name]; overriding {
messages = append(messages, fmt.Sprintf("Jenkins Master container env '%s' cannot be overridden", userEnv.Name))
}
}
requiredFlags := map[string]bool{
"-Djenkins.install.runSetupWizard=false": false,
"-Djava.awt.headless=true": false,
}
for _, setFlag := range strings.Split(javaOpts.Value, " ") {
for requiredFlag := range requiredFlags {
if setFlag == requiredFlag {
requiredFlags[requiredFlag] = true
break
}
}
}
for requiredFlag, set := range requiredFlags {
if !set {
messages = append(messages, fmt.Sprintf("Jenkins Master container env '%s' doesn't have required flag '%s'", constants.JavaOpsVariableName, requiredFlag))
}
}
return messages
}
func (r *ReconcileJenkinsBaseConfiguration) validatePlugins(requiredBasePlugins []plugins.Plugin, basePlugins, userPlugins []v1alpha2.Plugin) []string {
var messages []string
allPlugins := map[plugins.Plugin][]plugins.Plugin{}
for _, jenkinsPlugin := range basePlugins {
plugin, err := plugins.NewPlugin(jenkinsPlugin.Name, jenkinsPlugin.Version, jenkinsPlugin.DownloadURL)
if err != nil {
messages = append(messages, err.Error())
}
if plugin != nil {
allPlugins[*plugin] = []plugins.Plugin{}
}
}
for _, jenkinsPlugin := range userPlugins {
plugin, err := plugins.NewPlugin(jenkinsPlugin.Name, jenkinsPlugin.Version, jenkinsPlugin.DownloadURL)
if err != nil {
messages = append(messages, err.Error())
}
if plugin != nil {
allPlugins[*plugin] = []plugins.Plugin{}
}
}
if msg := plugins.VerifyDependencies(allPlugins); len(msg) > 0 {
messages = append(messages, msg...)
}
if msg := r.verifyBasePlugins(requiredBasePlugins, basePlugins); len(msg) > 0 {
messages = append(messages, msg...)
}
return messages
}
func (r *ReconcileJenkinsBaseConfiguration) verifyBasePlugins(requiredBasePlugins []plugins.Plugin, basePlugins []v1alpha2.Plugin) []string {
var messages []string
for _, requiredBasePlugin := range requiredBasePlugins {
found := false
for _, basePlugin := range basePlugins {
if requiredBasePlugin.Name == basePlugin.Name {
found = true
break
}
}
if !found {
messages = append(messages, fmt.Sprintf("Missing plugin '%s' in spec.master.basePlugins", requiredBasePlugin.Name))
}
}
return messages
}
func (r *ReconcileJenkinsBaseConfiguration) validateCustomization(customization v1alpha2.Customization, name string) ([]string, error) {
var messages []string
if len(customization.Secret.Name) == 0 && len(customization.Configurations) == 0 {
return nil, nil
}
if len(customization.Secret.Name) > 0 && len(customization.Configurations) == 0 {
messages = append(messages, fmt.Sprintf("%s.secret.name is set but %s.configurations is empty", name, name))
}
if len(customization.Secret.Name) > 0 {
secret := &corev1.Secret{}
err := r.Client.Get(context.TODO(), types.NamespacedName{Name: customization.Secret.Name, Namespace: r.Configuration.Jenkins.ObjectMeta.Namespace}, secret)
if err != nil && apierrors.IsNotFound(err) {
messages = append(messages, fmt.Sprintf("Secret '%s' configured in %s.secret.name not found", customization.Secret.Name, name))
} else if err != nil && !apierrors.IsNotFound(err) {
return nil, stackerr.WithStack(err)
}
}
for index, configMapRef := range customization.Configurations {
if len(configMapRef.Name) == 0 {
messages = append(messages, fmt.Sprintf("%s.configurations[%d] name is empty", name, index))
continue
}
configMap := &corev1.ConfigMap{}
err := r.Client.Get(context.TODO(), types.NamespacedName{Name: configMapRef.Name, Namespace: r.Configuration.Jenkins.ObjectMeta.Namespace}, configMap)
if err != nil && apierrors.IsNotFound(err) {
messages = append(messages, fmt.Sprintf("ConfigMap '%s' configured in %s.configurations[%d] not found", configMapRef.Name, name, index))
} else if err != nil && !apierrors.IsNotFound(err) {
return nil, stackerr.WithStack(err)
}
}
return messages, nil
}
@@ -1,964 +0,0 @@
package base
import (
"context"
"fmt"
"testing"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/plugins"
"github.com/jenkinsci/kubernetes-operator/pkg/log"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
const defaultNamespace = "default"
func TestValidatePlugins(t *testing.T) {
log.SetupLogger(true)
jenkins := &v1alpha2.Jenkins{ObjectMeta: metav1.ObjectMeta{Name: "example"}}
baseReconcileLoop := New(configuration.Configuration{Jenkins: jenkins}, client.JenkinsAPIConnectionSettings{})
t.Run("empty", func(t *testing.T) {
var requiredBasePlugins []plugins.Plugin
var basePlugins []v1alpha2.Plugin
var userPlugins []v1alpha2.Plugin
got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins)
assert.Nil(t, got)
})
t.Run("valid user plugin", func(t *testing.T) {
var requiredBasePlugins []plugins.Plugin
var basePlugins []v1alpha2.Plugin
userPlugins := []v1alpha2.Plugin{{Name: "simple-plugin", Version: "0.0.1"}}
got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins)
assert.Nil(t, got)
})
t.Run("invalid user plugin name", func(t *testing.T) {
var requiredBasePlugins []plugins.Plugin
var basePlugins []v1alpha2.Plugin
userPlugins := []v1alpha2.Plugin{{Name: "INVALID?", Version: "0.0.1"}}
got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins)
assert.Equal(t, got, []string{"invalid plugin name 'INVALID?:0.0.1', must follow pattern '" + plugins.NamePattern.String() + "'"})
})
t.Run("invalid user plugin version", func(t *testing.T) {
var requiredBasePlugins []plugins.Plugin
var basePlugins []v1alpha2.Plugin
userPlugins := []v1alpha2.Plugin{{Name: "simple-plugin", Version: "invalid!"}}
got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins)
assert.Equal(t, got, []string{"invalid plugin version 'simple-plugin:invalid!', must follow pattern '" + plugins.VersionPattern.String() + "'"})
})
t.Run("valid base plugin", func(t *testing.T) {
var requiredBasePlugins []plugins.Plugin
basePlugins := []v1alpha2.Plugin{{Name: "simple-plugin", Version: "0.0.1"}}
var userPlugins []v1alpha2.Plugin
got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins)
assert.Nil(t, got)
})
t.Run("invalid base plugin name", func(t *testing.T) {
var requiredBasePlugins []plugins.Plugin
basePlugins := []v1alpha2.Plugin{{Name: "INVALID?", Version: "0.0.1"}}
var userPlugins []v1alpha2.Plugin
got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins)
assert.Equal(t, got, []string{"invalid plugin name 'INVALID?:0.0.1', must follow pattern '" + plugins.NamePattern.String() + "'"})
})
t.Run("invalid base plugin version", func(t *testing.T) {
var requiredBasePlugins []plugins.Plugin
basePlugins := []v1alpha2.Plugin{{Name: "simple-plugin", Version: "invalid!"}}
var userPlugins []v1alpha2.Plugin
got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins)
assert.Equal(t, got, []string{"invalid plugin version 'simple-plugin:invalid!', must follow pattern '" + plugins.VersionPattern.String() + "'"})
})
t.Run("valid user and base plugin version", func(t *testing.T) {
var requiredBasePlugins []plugins.Plugin
basePlugins := []v1alpha2.Plugin{{Name: "simple-plugin", Version: "0.0.1"}}
userPlugins := []v1alpha2.Plugin{{Name: "simple-plugin", Version: "0.0.1"}}
got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins)
assert.Nil(t, got)
})
t.Run("invalid user and base plugin version", func(t *testing.T) {
var requiredBasePlugins []plugins.Plugin
basePlugins := []v1alpha2.Plugin{{Name: "simple-plugin", Version: "0.0.1"}}
userPlugins := []v1alpha2.Plugin{{Name: "simple-plugin", Version: "0.0.2"}}
got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins)
assert.Contains(t, got, "Plugin 'simple-plugin:0.0.1' requires version '0.0.1' but plugin 'simple-plugin:0.0.2' requires '0.0.2' for plugin 'simple-plugin'")
assert.Contains(t, got, "Plugin 'simple-plugin:0.0.2' requires version '0.0.2' but plugin 'simple-plugin:0.0.1' requires '0.0.1' for plugin 'simple-plugin'")
})
t.Run("required base plugin set with the same version", func(t *testing.T) {
requiredBasePlugins := []plugins.Plugin{{Name: "simple-plugin", Version: "0.0.1"}}
basePlugins := []v1alpha2.Plugin{{Name: "simple-plugin", Version: "0.0.1"}}
var userPlugins []v1alpha2.Plugin
got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins)
assert.Nil(t, got)
})
t.Run("required base plugin set with different version", func(t *testing.T) {
requiredBasePlugins := []plugins.Plugin{{Name: "simple-plugin", Version: "0.0.1"}}
basePlugins := []v1alpha2.Plugin{{Name: "simple-plugin", Version: "0.0.2"}}
var userPlugins []v1alpha2.Plugin
got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins)
assert.Nil(t, got)
})
t.Run("missing required base plugin", func(t *testing.T) {
requiredBasePlugins := []plugins.Plugin{{Name: "simple-plugin", Version: "0.0.1"}}
var basePlugins []v1alpha2.Plugin
var userPlugins []v1alpha2.Plugin
got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins)
assert.Equal(t, got, []string{"Missing plugin 'simple-plugin' in spec.master.basePlugins"})
})
}
func TestReconcileJenkinsBaseConfiguration_validateImagePullSecrets(t *testing.T) {
t.Run("happy", func(t *testing.T) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-ref",
},
Data: map[string][]byte{
"docker-server": []byte("test_server"),
"docker-username": []byte("test_user"),
"docker-password": []byte("test_password"),
"docker-email": []byte("test_email"),
},
}
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
ImagePullSecrets: []corev1.LocalObjectReference{
{Name: secret.ObjectMeta.Name},
},
},
},
}
fakeClient := fake.NewFakeClient()
err := fakeClient.Create(context.TODO(), secret)
assert.NoError(t, err)
baseReconcileLoop := New(configuration.Configuration{
Client: fakeClient,
Jenkins: &jenkins,
}, client.JenkinsAPIConnectionSettings{})
got, err := baseReconcileLoop.validateImagePullSecrets()
fmt.Println(got)
assert.Nil(t, got)
assert.NoError(t, err)
})
t.Run("no secret", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
ImagePullSecrets: []corev1.LocalObjectReference{
{Name: "test-ref"},
},
},
},
}
fakeClient := fake.NewFakeClient()
baseReconcileLoop := New(configuration.Configuration{
Client: fakeClient,
Jenkins: &jenkins,
}, client.JenkinsAPIConnectionSettings{})
got, _ := baseReconcileLoop.validateImagePullSecrets()
assert.Equal(t, got, []string{"Secret test-ref not found defined in spec.master.imagePullSecrets", "Secret 'test-ref' defined in spec.master.imagePullSecrets doesn't have 'docker-server' key.", "Secret 'test-ref' defined in spec.master.imagePullSecrets doesn't have 'docker-username' key.", "Secret 'test-ref' defined in spec.master.imagePullSecrets doesn't have 'docker-password' key.", "Secret 'test-ref' defined in spec.master.imagePullSecrets doesn't have 'docker-email' key."})
})
t.Run("no docker email", func(t *testing.T) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-ref",
},
Data: map[string][]byte{
"docker-server": []byte("test_server"),
"docker-username": []byte("test_user"),
"docker-password": []byte("test_password"),
},
}
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
ImagePullSecrets: []corev1.LocalObjectReference{
{Name: secret.ObjectMeta.Name},
},
},
},
}
fakeClient := fake.NewFakeClient()
err := fakeClient.Create(context.TODO(), secret)
assert.NoError(t, err)
baseReconcileLoop := New(configuration.Configuration{
Client: fakeClient,
Jenkins: &jenkins,
}, client.JenkinsAPIConnectionSettings{})
got, _ := baseReconcileLoop.validateImagePullSecrets()
assert.Equal(t, got, []string{"Secret 'test-ref' defined in spec.master.imagePullSecrets doesn't have 'docker-email' key."})
})
t.Run("no docker password", func(t *testing.T) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-ref",
},
Data: map[string][]byte{
"docker-server": []byte("test_server"),
"docker-username": []byte("test_user"),
"docker-email": []byte("test_email"),
},
}
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
ImagePullSecrets: []corev1.LocalObjectReference{
{Name: secret.ObjectMeta.Name},
},
},
},
}
fakeClient := fake.NewFakeClient()
err := fakeClient.Create(context.TODO(), secret)
assert.NoError(t, err)
baseReconcileLoop := New(configuration.Configuration{
Client: fakeClient,
Jenkins: &jenkins,
}, client.JenkinsAPIConnectionSettings{})
got, _ := baseReconcileLoop.validateImagePullSecrets()
assert.Equal(t, got, []string{"Secret 'test-ref' defined in spec.master.imagePullSecrets doesn't have 'docker-password' key."})
})
t.Run("no docker username", func(t *testing.T) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-ref",
},
Data: map[string][]byte{
"docker-server": []byte("test_server"),
"docker-password": []byte("test_password"),
"docker-email": []byte("test_email"),
},
}
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
ImagePullSecrets: []corev1.LocalObjectReference{
{Name: secret.ObjectMeta.Name},
},
},
},
}
fakeClient := fake.NewFakeClient()
err := fakeClient.Create(context.TODO(), secret)
assert.NoError(t, err)
baseReconcileLoop := New(configuration.Configuration{
Client: fakeClient,
Jenkins: &jenkins,
}, client.JenkinsAPIConnectionSettings{})
got, _ := baseReconcileLoop.validateImagePullSecrets()
assert.Equal(t, got, []string{"Secret 'test-ref' defined in spec.master.imagePullSecrets doesn't have 'docker-username' key."})
})
t.Run("no docker server", func(t *testing.T) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-ref",
},
Data: map[string][]byte{
"docker-username": []byte("test_user"),
"docker-password": []byte("test_password"),
"docker-email": []byte("test_email"),
},
}
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
ImagePullSecrets: []corev1.LocalObjectReference{
{Name: secret.ObjectMeta.Name},
},
},
},
}
fakeClient := fake.NewFakeClient()
err := fakeClient.Create(context.TODO(), secret)
assert.NoError(t, err)
baseReconcileLoop := New(configuration.Configuration{
Client: fakeClient,
Jenkins: &jenkins,
}, client.JenkinsAPIConnectionSettings{})
got, _ := baseReconcileLoop.validateImagePullSecrets()
assert.Equal(t, got, []string{"Secret 'test-ref' defined in spec.master.imagePullSecrets doesn't have 'docker-server' key."})
})
}
func TestValidateJenkinsMasterPodEnvs(t *testing.T) {
validJenkinsOps := "-Djenkins.install.runSetupWizard=false -Djava.awt.headless=true"
t.Run("happy", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
Containers: []v1alpha2.Container{
{
Env: []corev1.EnvVar{
{
Name: "SOME_VALUE",
Value: "",
},
{
Name: constants.JavaOpsVariableName,
Value: validJenkinsOps,
},
},
},
},
},
},
}
baseReconcileLoop := New(configuration.Configuration{
Jenkins: &jenkins,
}, client.JenkinsAPIConnectionSettings{})
got := baseReconcileLoop.validateJenkinsMasterPodEnvs()
assert.Nil(t, got)
})
t.Run("missing -Djava.awt.headless=true in JAVA_OPTS env", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
Containers: []v1alpha2.Container{
{
Env: []corev1.EnvVar{
{
Name: constants.JavaOpsVariableName,
Value: "-Djenkins.install.runSetupWizard=false",
},
},
},
},
},
},
}
baseReconcileLoop := New(configuration.Configuration{
Jenkins: &jenkins,
}, client.JenkinsAPIConnectionSettings{})
got := baseReconcileLoop.validateJenkinsMasterPodEnvs()
assert.Equal(t, got, []string{"Jenkins Master container env 'JAVA_OPTS' doesn't have required flag '-Djava.awt.headless=true'"})
})
t.Run("missing -Djenkins.install.runSetupWizard=false in JAVA_OPTS env", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
Containers: []v1alpha2.Container{
{
Env: []corev1.EnvVar{
{
Name: constants.JavaOpsVariableName,
Value: "-Djava.awt.headless=true",
},
},
},
},
},
},
}
baseReconcileLoop := New(configuration.Configuration{
Jenkins: &jenkins,
}, client.JenkinsAPIConnectionSettings{})
got := baseReconcileLoop.validateJenkinsMasterPodEnvs()
assert.Equal(t, got, []string{"Jenkins Master container env 'JAVA_OPTS' doesn't have required flag '-Djenkins.install.runSetupWizard=false'"})
})
}
func TestValidateReservedVolumes(t *testing.T) {
t.Run("happy", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
Volumes: []corev1.Volume{
{
Name: "not-used-name",
},
},
},
},
}
baseReconcileLoop := New(configuration.Configuration{
Jenkins: &jenkins,
}, client.JenkinsAPIConnectionSettings{})
got := baseReconcileLoop.validateReservedVolumes()
assert.Nil(t, got)
})
t.Run("used reserved name", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
Volumes: []corev1.Volume{
{
Name: resources.JenkinsHomeVolumeName,
},
},
},
},
}
baseReconcileLoop := New(configuration.Configuration{
Jenkins: &jenkins,
}, client.JenkinsAPIConnectionSettings{})
got := baseReconcileLoop.validateReservedVolumes()
assert.Equal(t, got, []string{"Jenkins Master pod volume 'jenkins-home' is reserved please choose different one"})
})
}
func TestValidateContainerVolumeMounts(t *testing.T) {
t.Run("default Jenkins master container", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{},
},
}
baseReconcileLoop := New(configuration.Configuration{
Jenkins: &jenkins,
}, client.JenkinsAPIConnectionSettings{})
got := baseReconcileLoop.validateContainerVolumeMounts(v1alpha2.Container{})
assert.Nil(t, got)
})
t.Run("one extra volume", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
Volumes: []corev1.Volume{
{
Name: "example",
},
},
Containers: []v1alpha2.Container{
{
VolumeMounts: []corev1.VolumeMount{
{
Name: "example",
MountPath: "/test",
},
},
},
},
},
},
}
baseReconcileLoop := New(configuration.Configuration{
Jenkins: &jenkins,
}, client.JenkinsAPIConnectionSettings{})
got := baseReconcileLoop.validateContainerVolumeMounts(jenkins.Spec.Master.Containers[0])
assert.Nil(t, got)
})
t.Run("empty mountPath", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
Volumes: []corev1.Volume{
{
Name: "example",
},
},
Containers: []v1alpha2.Container{
{
VolumeMounts: []corev1.VolumeMount{
{
Name: "example",
MountPath: "", // empty
},
},
},
},
},
},
}
baseReconcileLoop := New(configuration.Configuration{
Jenkins: &jenkins,
}, client.JenkinsAPIConnectionSettings{})
got := baseReconcileLoop.validateContainerVolumeMounts(jenkins.Spec.Master.Containers[0])
assert.Equal(t, got, []string{"mountPath not set for 'example' volume mount in container ''"})
})
t.Run("missing volume", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
Containers: []v1alpha2.Container{
{
VolumeMounts: []corev1.VolumeMount{
{
Name: "missing-volume",
MountPath: "/test",
},
},
},
},
},
},
}
baseReconcileLoop := New(configuration.Configuration{
Jenkins: &jenkins,
}, client.JenkinsAPIConnectionSettings{})
got := baseReconcileLoop.validateContainerVolumeMounts(jenkins.Spec.Master.Containers[0])
assert.Equal(t, got, []string{"Not found volume for 'missing-volume' volume mount in container ''"})
})
}
func TestValidateConfigMapVolume(t *testing.T) {
t.Run("optional", func(t *testing.T) {
optional := true
volume := corev1.Volume{
Name: "name",
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
Optional: &optional,
},
},
}
fakeClient := fake.NewFakeClient()
baseReconcileLoop := New(configuration.Configuration{
Jenkins: &v1alpha2.Jenkins{ObjectMeta: metav1.ObjectMeta{Name: "example"}},
Client: fakeClient,
}, client.JenkinsAPIConnectionSettings{})
got, err := baseReconcileLoop.validateConfigMapVolume(volume)
assert.NoError(t, err)
assert.Nil(t, got)
})
t.Run("happy, required", func(t *testing.T) {
optional := false
configMap := corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: defaultNamespace, Name: "configmap-name"}}
jenkins := &v1alpha2.Jenkins{ObjectMeta: metav1.ObjectMeta{Namespace: defaultNamespace}}
volume := corev1.Volume{
Name: "volume-name",
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
Optional: &optional,
LocalObjectReference: corev1.LocalObjectReference{
Name: configMap.Name,
},
},
},
}
fakeClient := fake.NewFakeClient()
err := fakeClient.Create(context.TODO(), &configMap)
assert.NoError(t, err)
baseReconcileLoop := New(configuration.Configuration{
Client: fakeClient,
Jenkins: jenkins,
}, client.JenkinsAPIConnectionSettings{})
got, err := baseReconcileLoop.validateConfigMapVolume(volume)
assert.NoError(t, err)
assert.Nil(t, got)
})
t.Run("missing configmap", func(t *testing.T) {
optional := false
configMap := corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: defaultNamespace, Name: "configmap-name"}}
jenkins := &v1alpha2.Jenkins{ObjectMeta: metav1.ObjectMeta{Namespace: defaultNamespace}}
volume := corev1.Volume{
Name: "volume-name",
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
Optional: &optional,
LocalObjectReference: corev1.LocalObjectReference{
Name: configMap.Name,
},
},
},
}
fakeClient := fake.NewFakeClient()
baseReconcileLoop := New(configuration.Configuration{
Client: fakeClient,
Jenkins: jenkins,
}, client.JenkinsAPIConnectionSettings{})
got, err := baseReconcileLoop.validateConfigMapVolume(volume)
assert.NoError(t, err)
assert.Equal(t, got, []string{"ConfigMap 'configmap-name' not found for volume '{volume-name {nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil &ConfigMapVolumeSource{LocalObjectReference:LocalObjectReference{Name:configmap-name,},Items:[]KeyToPath{},DefaultMode:nil,Optional:*false,} nil nil nil nil nil nil nil nil nil}}'"})
})
}
func TestValidateSecretVolume(t *testing.T) {
t.Run("optional", func(t *testing.T) {
optional := true
volume := corev1.Volume{
Name: "name",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
Optional: &optional,
},
},
}
fakeClient := fake.NewFakeClient()
baseReconcileLoop := New(configuration.Configuration{
Jenkins: &v1alpha2.Jenkins{ObjectMeta: metav1.ObjectMeta{Name: "example"}},
Client: fakeClient,
}, client.JenkinsAPIConnectionSettings{})
got, err := baseReconcileLoop.validateSecretVolume(volume)
assert.NoError(t, err)
assert.Nil(t, got)
})
t.Run("happy, required", func(t *testing.T) {
optional := false
secret := corev1.Secret{ObjectMeta: metav1.ObjectMeta{Namespace: defaultNamespace, Name: "secret-name"}}
jenkins := &v1alpha2.Jenkins{ObjectMeta: metav1.ObjectMeta{Namespace: defaultNamespace}}
volume := corev1.Volume{
Name: "volume-name",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
Optional: &optional,
SecretName: secret.Name,
},
},
}
fakeClient := fake.NewFakeClient()
err := fakeClient.Create(context.TODO(), &secret)
assert.NoError(t, err)
baseReconcileLoop := New(configuration.Configuration{
Client: fakeClient,
Jenkins: jenkins,
}, client.JenkinsAPIConnectionSettings{})
got, err := baseReconcileLoop.validateSecretVolume(volume)
assert.NoError(t, err)
assert.Nil(t, got)
})
t.Run("missing secret", func(t *testing.T) {
optional := false
secret := corev1.Secret{ObjectMeta: metav1.ObjectMeta{Namespace: defaultNamespace, Name: "secret-name"}}
jenkins := &v1alpha2.Jenkins{ObjectMeta: metav1.ObjectMeta{Namespace: defaultNamespace}}
volume := corev1.Volume{
Name: "volume-name",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
Optional: &optional,
SecretName: secret.Name,
},
},
}
fakeClient := fake.NewFakeClient()
baseReconcileLoop := New(configuration.Configuration{
Client: fakeClient,
Jenkins: jenkins,
}, client.JenkinsAPIConnectionSettings{})
got, err := baseReconcileLoop.validateSecretVolume(volume)
assert.NoError(t, err)
assert.Equal(t, got, []string{"Secret 'secret-name' not found for volume '{volume-name {nil nil nil nil nil &SecretVolumeSource{SecretName:secret-name,Items:[]KeyToPath{},DefaultMode:nil,Optional:*false,} nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil}}'"})
})
}
func TestValidateCustomization(t *testing.T) {
secretName := "secretName"
configMapName := "configmap-name"
jenkins := &v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Namespace: defaultNamespace,
},
}
t.Run("empty", func(t *testing.T) {
customization := v1alpha2.Customization{}
fakeClient := fake.NewFakeClient()
baseReconcileLoop := New(configuration.Configuration{
Jenkins: jenkins,
Client: fakeClient,
}, client.JenkinsAPIConnectionSettings{})
got, err := baseReconcileLoop.validateCustomization(customization, "spec.groovyScripts")
assert.NoError(t, err)
assert.Nil(t, got)
})
t.Run("secret set but configurations is empty", func(t *testing.T) {
customization := v1alpha2.Customization{
Secret: v1alpha2.SecretRef{Name: secretName},
Configurations: []v1alpha2.ConfigMapRef{},
}
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: defaultNamespace,
},
}
fakeClient := fake.NewFakeClient()
baseReconcileLoop := New(configuration.Configuration{
Jenkins: jenkins,
Client: fakeClient,
}, client.JenkinsAPIConnectionSettings{})
err := fakeClient.Create(context.TODO(), secret)
require.NoError(t, err)
got, err := baseReconcileLoop.validateCustomization(customization, "spec.groovyScripts")
assert.NoError(t, err)
assert.Equal(t, got, []string{"spec.groovyScripts.secret.name is set but spec.groovyScripts.configurations is empty"})
})
t.Run("secret and configmap exists", func(t *testing.T) {
customization := v1alpha2.Customization{
Secret: v1alpha2.SecretRef{Name: secretName},
Configurations: []v1alpha2.ConfigMapRef{{Name: configMapName}},
}
configMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: configMapName,
Namespace: defaultNamespace,
},
}
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: defaultNamespace,
},
}
fakeClient := fake.NewFakeClient()
baseReconcileLoop := New(configuration.Configuration{
Jenkins: jenkins,
Client: fakeClient,
}, client.JenkinsAPIConnectionSettings{})
err := fakeClient.Create(context.TODO(), secret)
require.NoError(t, err)
err = fakeClient.Create(context.TODO(), configMap)
require.NoError(t, err)
got, err := baseReconcileLoop.validateCustomization(customization, "spec.groovyScripts")
assert.NoError(t, err)
assert.Nil(t, got)
})
t.Run("secret not exists and configmap exists", func(t *testing.T) {
configMapName := "configmap-name"
customization := v1alpha2.Customization{
Secret: v1alpha2.SecretRef{Name: secretName},
Configurations: []v1alpha2.ConfigMapRef{{Name: configMapName}},
}
configMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: configMapName,
Namespace: defaultNamespace,
},
}
fakeClient := fake.NewFakeClient()
baseReconcileLoop := New(configuration.Configuration{
Jenkins: jenkins,
Client: fakeClient,
}, client.JenkinsAPIConnectionSettings{})
err := fakeClient.Create(context.TODO(), configMap)
require.NoError(t, err)
got, err := baseReconcileLoop.validateCustomization(customization, "spec.groovyScripts")
assert.NoError(t, err)
assert.Equal(t, got, []string{"Secret 'secretName' configured in spec.groovyScripts.secret.name not found"})
})
t.Run("secret exists and configmap not exists", func(t *testing.T) {
customization := v1alpha2.Customization{
Secret: v1alpha2.SecretRef{Name: secretName},
Configurations: []v1alpha2.ConfigMapRef{{Name: configMapName}},
}
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: defaultNamespace,
},
}
fakeClient := fake.NewFakeClient()
baseReconcileLoop := New(configuration.Configuration{
Jenkins: jenkins,
Client: fakeClient,
}, client.JenkinsAPIConnectionSettings{})
err := fakeClient.Create(context.TODO(), secret)
require.NoError(t, err)
got, err := baseReconcileLoop.validateCustomization(customization, "spec.groovyScripts")
assert.NoError(t, err)
assert.Equal(t, got, []string{"ConfigMap 'configmap-name' configured in spec.groovyScripts.configurations[0] not found"})
})
}
func TestValidateJenkinsMasterContainerCommand(t *testing.T) {
log.SetupLogger(true)
t.Run("no Jenkins master container", func(t *testing.T) {
jenkins := &v1alpha2.Jenkins{}
baseReconcileLoop := New(configuration.Configuration{Jenkins: jenkins}, client.JenkinsAPIConnectionSettings{})
got := baseReconcileLoop.validateJenkinsMasterContainerCommand()
assert.Empty(t, got)
})
t.Run("empty command", func(t *testing.T) {
jenkins := &v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
Containers: []v1alpha2.Container{
{
Name: resources.JenkinsMasterContainerName,
},
},
},
},
}
baseReconcileLoop := New(configuration.Configuration{Jenkins: jenkins}, client.JenkinsAPIConnectionSettings{})
got := baseReconcileLoop.validateJenkinsMasterContainerCommand()
assert.Len(t, got, 1)
})
t.Run("command has 3 lines but it's values are invalid", func(t *testing.T) {
jenkins := &v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
Containers: []v1alpha2.Container{
{
Name: resources.JenkinsMasterContainerName,
Command: []string{
"invalid",
"invalid",
"invalid",
},
},
},
},
},
}
baseReconcileLoop := New(configuration.Configuration{Jenkins: jenkins}, client.JenkinsAPIConnectionSettings{})
got := baseReconcileLoop.validateJenkinsMasterContainerCommand()
assert.Len(t, got, 1)
})
t.Run("valid command", func(t *testing.T) {
jenkins := &v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
Containers: []v1alpha2.Container{
{
Name: resources.JenkinsMasterContainerName,
Command: resources.GetJenkinsMasterContainerBaseCommand(),
},
},
},
},
}
baseReconcileLoop := New(configuration.Configuration{Jenkins: jenkins}, client.JenkinsAPIConnectionSettings{})
got := baseReconcileLoop.validateJenkinsMasterContainerCommand()
assert.Len(t, got, 0)
})
t.Run("custom valid command", func(t *testing.T) {
jenkins := &v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
Containers: []v1alpha2.Container{
{
Name: resources.JenkinsMasterContainerName,
Command: []string{
"bash",
"-c",
fmt.Sprintf("%s/%s && my-extra-command.sh && exec /sbin/tini -s -- /usr/local/bin/jenkins.sh",
resources.JenkinsScriptsVolumePath, resources.InitScriptName),
},
},
},
},
},
}
baseReconcileLoop := New(configuration.Configuration{Jenkins: jenkins}, client.JenkinsAPIConnectionSettings{})
got := baseReconcileLoop.validateJenkinsMasterContainerCommand()
assert.Len(t, got, 0)
})
t.Run("no exec command for the Jenkins", func(t *testing.T) {
jenkins := &v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
Containers: []v1alpha2.Container{
{
Name: resources.JenkinsMasterContainerName,
Command: []string{
"bash",
"-c",
fmt.Sprintf("%s/%s && my-extra-command.sh && /sbin/tini -s -- /usr/local/bin/jenkins.sh",
resources.JenkinsScriptsVolumePath, resources.InitScriptName),
},
},
},
},
},
}
baseReconcileLoop := New(configuration.Configuration{Jenkins: jenkins}, client.JenkinsAPIConnectionSettings{})
got := baseReconcileLoop.validateJenkinsMasterContainerCommand()
assert.Len(t, got, 1)
})
}
@@ -1,305 +0,0 @@
package configuration
import (
"bytes"
"context"
"strings"
"time"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/event"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/reason"
stackerr "github.com/pkg/errors"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/remotecommand"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)
// Configuration holds required for Jenkins configuration.
type Configuration struct {
Client client.Client
ClientSet kubernetes.Clientset
Notifications *chan event.Event
Jenkins *v1alpha2.Jenkins
Scheme *runtime.Scheme
Config *rest.Config
JenkinsAPIConnectionSettings jenkinsclient.JenkinsAPIConnectionSettings
}
// RestartJenkinsMasterPod terminate Jenkins master pod and notifies about it.
func (c *Configuration) RestartJenkinsMasterPod(reason reason.Reason) error {
currentJenkinsMasterPod, err := c.GetJenkinsMasterPod()
if err != nil {
return err
}
if c.IsJenkinsTerminating(*currentJenkinsMasterPod) {
return nil
}
*c.Notifications <- event.Event{
Jenkins: *c.Jenkins,
Phase: event.PhaseBase,
Level: v1alpha2.NotificationLevelInfo,
Reason: reason,
}
return stackerr.WithStack(c.Client.Delete(context.TODO(), currentJenkinsMasterPod))
}
// GetJenkinsMasterPod gets the jenkins master pod.
func (c *Configuration) GetJenkinsMasterPod() (*corev1.Pod, error) {
jenkinsMasterPodName := resources.GetJenkinsMasterPodName(c.Jenkins)
currentJenkinsMasterPod := &corev1.Pod{}
err := c.Client.Get(context.TODO(), types.NamespacedName{Name: jenkinsMasterPodName, Namespace: c.Jenkins.Namespace}, currentJenkinsMasterPod)
if err != nil {
return nil, err // don't wrap error
}
return currentJenkinsMasterPod, nil
}
// GetJenkinsMasterPod gets the jenkins master pod.
func (c *Configuration) GetJenkinsDeployment() (*appsv1.Deployment, error) {
jenkinsDeploymentName := resources.GetJenkinsDeploymentName(c.Jenkins)
currentJenkinsDeployment := &appsv1.Deployment{}
err := c.Client.Get(context.TODO(), types.NamespacedName{Name: jenkinsDeploymentName, Namespace: c.Jenkins.Namespace}, currentJenkinsDeployment)
if err != nil {
return nil, stackerr.WithStack(err)
}
return currentJenkinsDeployment, nil
}
// IsJenkinsTerminating returns true if the Jenkins pod is terminating.
func (c *Configuration) IsJenkinsTerminating(pod corev1.Pod) bool {
return pod.ObjectMeta.DeletionTimestamp != nil
}
// CreateResource is creating kubernetes resource and references it to Jenkins CR
func (c *Configuration) CreateResource(obj metav1.Object) error {
runtimeObj, ok := obj.(runtime.Object)
if !ok {
return stackerr.Errorf("is not a %T a runtime.Object", obj)
}
// Set Jenkins instance as the owner and controller.
if err := controllerutil.SetControllerReference(c.Jenkins, obj, c.Scheme); err != nil {
return stackerr.WithStack(err)
}
return c.Client.Create(context.TODO(), runtimeObj) // don't wrap error
}
// UpdateResource is updating kubernetes resource and references it to Jenkins CR.
func (c *Configuration) UpdateResource(obj metav1.Object) error {
runtimeObj, ok := obj.(runtime.Object)
if !ok {
return stackerr.Errorf("is not a %T a runtime.Object", obj)
}
// set Jenkins instance as the owner and controller, don't check error(can be already set)
_ = controllerutil.SetControllerReference(c.Jenkins, obj, c.Scheme)
return c.Client.Update(context.TODO(), runtimeObj) // don't wrap error
}
// CreateOrUpdateResource is creating or updating kubernetes resource and references it to Jenkins CR.
func (c *Configuration) CreateOrUpdateResource(obj metav1.Object) error {
runtimeObj, ok := obj.(runtime.Object)
if !ok {
return stackerr.Errorf("is not a %T a runtime.Object", obj)
}
// set Jenkins instance as the owner and controller, don't check error(can be already set)
_ = controllerutil.SetControllerReference(c.Jenkins, obj, c.Scheme)
err := c.Client.Create(context.TODO(), runtimeObj)
if err != nil && errors.IsAlreadyExists(err) {
return c.UpdateResource(obj)
} else if err != nil && !errors.IsAlreadyExists(err) {
return stackerr.WithStack(err)
}
return nil
}
// Exec executes command in the given pod and it's container.
func (c *Configuration) Exec(podName, containerName string, command []string) (stdout, stderr bytes.Buffer, err error) {
req := c.ClientSet.CoreV1().RESTClient().Post().
Resource("pods").
Name(podName).
Namespace(c.Jenkins.Namespace).
SubResource("exec")
req.VersionedParams(&corev1.PodExecOptions{
Command: command,
Container: containerName,
Stdin: false,
Stdout: true,
Stderr: true,
TTY: false,
}, scheme.ParameterCodec)
exec, err := remotecommand.NewSPDYExecutor(c.Config, "POST", req.URL())
if err != nil {
return stdout, stderr, stackerr.Wrap(err, "pod exec error while creating Executor")
}
err = exec.Stream(remotecommand.StreamOptions{
Stdin: nil,
Stdout: &stdout,
Stderr: &stderr,
Tty: false,
})
if err != nil {
return stdout, stderr, stackerr.Wrapf(err, "pod exec error operation on stream: stdout '%s' stderr '%s'", stdout.String(), stderr.String())
}
return
}
// GetJenkinsMasterContainer returns the Jenkins master container from the CR.
func (c *Configuration) GetJenkinsMasterContainer() *v1alpha2.Container {
if len(c.Jenkins.Spec.Master.Containers) > 0 {
// the first container is the Jenkins master, it is forced jenkins_controller.go
return &c.Jenkins.Spec.Master.Containers[0]
}
return nil
}
// GetJenkinsClient gets jenkins client from a configuration.
func (c *Configuration) GetJenkinsClient() (jenkinsclient.Jenkins, error) {
switch c.Jenkins.Spec.JenkinsAPISettings.AuthorizationStrategy {
case v1alpha2.ServiceAccountAuthorizationStrategy:
return c.GetJenkinsClientFromServiceAccount()
case v1alpha2.CreateUserAuthorizationStrategy:
return c.GetJenkinsClientFromSecret()
default:
return nil, stackerr.Errorf("unrecognized '%s' spec.jenkinsAPISettings.authorizationStrategy", c.Jenkins.Spec.JenkinsAPISettings.AuthorizationStrategy)
}
}
func (c *Configuration) getJenkinsAPIUrl() (string, error) {
var service corev1.Service
err := c.Client.Get(context.TODO(), types.NamespacedName{
Namespace: c.Jenkins.ObjectMeta.Namespace,
Name: resources.GetJenkinsHTTPServiceName(c.Jenkins),
}, &service)
if err != nil {
return "", err
}
jenkinsURL := c.JenkinsAPIConnectionSettings.BuildJenkinsAPIUrl(service.Name, service.Namespace, service.Spec.Ports[0].Port, service.Spec.Ports[0].NodePort)
if prefix, ok := GetJenkinsOpts(*c.Jenkins)["prefix"]; ok {
jenkinsURL += prefix
}
return jenkinsURL, nil
}
// GetJenkinsClientFromServiceAccount gets jenkins client from a serviceAccount.
func (c *Configuration) GetJenkinsClientFromServiceAccount() (jenkinsclient.Jenkins, error) {
jenkinsAPIUrl, err := c.getJenkinsAPIUrl()
if err != nil {
return nil, err
}
podName := resources.GetJenkinsMasterPodName(c.Jenkins)
token, _, err := c.Exec(podName, resources.JenkinsMasterContainerName, []string{"cat", "/var/run/secrets/kubernetes.io/serviceaccount/token"})
if err != nil {
return nil, err
}
return jenkinsclient.NewBearerTokenAuthorization(jenkinsAPIUrl, token.String())
}
// GetJenkinsClientFromSecret gets jenkins client from a secret.
func (c *Configuration) GetJenkinsClientFromSecret() (jenkinsclient.Jenkins, error) {
jenkinsURL, err := c.getJenkinsAPIUrl()
if err != nil {
return nil, err
}
credentialsSecret := &corev1.Secret{}
err = c.Client.Get(context.TODO(), types.NamespacedName{Name: resources.GetOperatorCredentialsSecretName(c.Jenkins), Namespace: c.Jenkins.ObjectMeta.Namespace}, credentialsSecret)
if err != nil {
return nil, stackerr.WithStack(err)
}
currentJenkinsMasterPod, err := c.GetJenkinsMasterPod()
if err != nil {
return nil, err
}
var tokenCreationTime *time.Time
tokenCreationTimeBytes := credentialsSecret.Data[resources.OperatorCredentialsSecretTokenCreationKey]
if tokenCreationTimeBytes != nil {
tokenCreationTime = &time.Time{}
err = tokenCreationTime.UnmarshalText(tokenCreationTimeBytes)
if err != nil {
tokenCreationTime = nil
}
}
if credentialsSecret.Data[resources.OperatorCredentialsSecretTokenKey] == nil ||
tokenCreationTimeBytes == nil || tokenCreationTime == nil ||
currentJenkinsMasterPod.ObjectMeta.CreationTimestamp.Time.UTC().After(tokenCreationTime.UTC()) {
userName := string(credentialsSecret.Data[resources.OperatorCredentialsSecretUserNameKey])
jenkinsClient, err := jenkinsclient.NewUserAndPasswordAuthorization(
jenkinsURL,
userName,
string(credentialsSecret.Data[resources.OperatorCredentialsSecretPasswordKey]))
if err != nil {
return nil, err
}
token, err := jenkinsClient.GenerateToken(userName, "token")
if err != nil {
return nil, err
}
credentialsSecret.Data[resources.OperatorCredentialsSecretTokenKey] = []byte(token.GetToken())
now, _ := time.Now().UTC().MarshalText()
credentialsSecret.Data[resources.OperatorCredentialsSecretTokenCreationKey] = now
err = c.UpdateResource(credentialsSecret)
if err != nil {
return nil, stackerr.WithStack(err)
}
}
return jenkinsclient.NewUserAndPasswordAuthorization(
jenkinsURL,
string(credentialsSecret.Data[resources.OperatorCredentialsSecretUserNameKey]),
string(credentialsSecret.Data[resources.OperatorCredentialsSecretTokenKey]))
}
// GetJenkinsOpts gets JENKINS_OPTS env parameter, parses it's values and returns it as a map`
func GetJenkinsOpts(jenkins v1alpha2.Jenkins) map[string]string {
envs := jenkins.Spec.Master.Containers[0].Env
jenkinsOpts := make(map[string]string)
for key, value := range envs {
if value.Name == "JENKINS_OPTS" {
jenkinsOptsEnv := envs[key]
jenkinsOptsWithDashes := jenkinsOptsEnv.Value
if len(jenkinsOptsWithDashes) == 0 {
return nil
}
jenkinsOptsWithEqOperators := strings.Split(jenkinsOptsWithDashes, " ")
for _, vx := range jenkinsOptsWithEqOperators {
opt := strings.Split(vx, "=")
jenkinsOpts[strings.ReplaceAll(opt[0], "--", "")] = opt[1]
}
return jenkinsOpts
}
}
return nil
}
@@ -1,144 +0,0 @@
package configuration
import (
"testing"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
)
func TestGetJenkinsOpts(t *testing.T) {
t.Run("JENKINS_OPTS is uninitialized", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
Containers: []v1alpha2.Container{
{
Env: []corev1.EnvVar{
{Name: "", Value: ""},
},
},
},
},
},
}
opts := GetJenkinsOpts(jenkins)
assert.Equal(t, 0, len(opts))
})
t.Run("JENKINS_OPTS is empty", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
Containers: []v1alpha2.Container{
{
Env: []corev1.EnvVar{
{Name: "JENKINS_OPTS", Value: ""},
},
},
},
},
},
}
opts := GetJenkinsOpts(jenkins)
assert.Equal(t, 0, len(opts))
})
t.Run("JENKINS_OPTS have --prefix argument ", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
Containers: []v1alpha2.Container{
{
Env: []corev1.EnvVar{
{Name: "JENKINS_OPTS", Value: "--prefix=/jenkins"},
},
},
},
},
},
}
opts := GetJenkinsOpts(jenkins)
assert.Equal(t, 1, len(opts))
assert.NotContains(t, opts, "httpPort")
assert.Contains(t, opts, "prefix")
assert.Equal(t, opts["prefix"], "/jenkins")
})
t.Run("JENKINS_OPTS have --prefix and --httpPort argument", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
Containers: []v1alpha2.Container{
{
Env: []corev1.EnvVar{
{Name: "JENKINS_OPTS", Value: "--prefix=/jenkins --httpPort=8080"},
},
},
},
},
},
}
opts := GetJenkinsOpts(jenkins)
assert.Equal(t, 2, len(opts))
assert.Contains(t, opts, "prefix")
assert.Equal(t, opts["prefix"], "/jenkins")
assert.Contains(t, opts, "httpPort")
assert.Equal(t, opts["httpPort"], "8080")
})
t.Run("JENKINS_OPTS have --httpPort argument", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
Containers: []v1alpha2.Container{
{
Env: []corev1.EnvVar{
{Name: "JENKINS_OPTS", Value: "--httpPort=8080"},
},
},
},
},
},
}
opts := GetJenkinsOpts(jenkins)
assert.Equal(t, 1, len(opts))
assert.NotContains(t, opts, "prefix")
assert.Contains(t, opts, "httpPort")
assert.Equal(t, opts["httpPort"], "8080")
})
t.Run("JENKINS_OPTS have --httpPort=--8080 argument", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
Containers: []v1alpha2.Container{
{
Env: []corev1.EnvVar{
{Name: "JENKINS_OPTS", Value: "--httpPort=--8080"},
},
},
},
},
},
}
opts := GetJenkinsOpts(jenkins)
assert.Equal(t, 1, len(opts))
assert.NotContains(t, opts, "prefix")
assert.Contains(t, opts, "httpPort")
assert.Equal(t, opts["httpPort"], "--8080")
})
}
@@ -1,86 +0,0 @@
package casc
import (
"fmt"
"strings"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/groovy"
k8s "sigs.k8s.io/controller-runtime/pkg/client"
)
const groovyUtf8MaxStringLength = 65535
// ConfigurationAsCode defines client for configurationAsCode
type ConfigurationAsCode interface {
Ensure(jenkins *v1alpha2.Jenkins) (requeue bool, err error)
}
type configurationAsCode struct {
groovyClient *groovy.Groovy
}
// New creates new instance of ConfigurationAsCode
func New(jenkinsClient jenkinsclient.Jenkins, k8sClient k8s.Client, jenkins *v1alpha2.Jenkins) ConfigurationAsCode {
return &configurationAsCode{
groovyClient: groovy.New(jenkinsClient, k8sClient, jenkins, "user-casc", jenkins.Spec.ConfigurationAsCode.Customization),
}
}
// Ensure configures Jenkins with help Configuration as a code plugin
func (c *configurationAsCode) Ensure(jenkins *v1alpha2.Jenkins) (requeue bool, err error) {
requeue, err = c.groovyClient.WaitForSecretSynchronization(resources.ConfigurationAsCodeSecretVolumePath)
if err != nil || requeue {
return requeue, err
}
return c.groovyClient.Ensure(func(name string) bool {
return strings.HasSuffix(name, ".yaml") || strings.HasSuffix(name, ".yml")
}, func(groovyScript string) string {
return fmt.Sprintf(applyConfigurationAsCodeGroovyScriptFmt, prepareScript(groovyScript))
})
}
const applyConfigurationAsCodeGroovyScriptFmt = `
String[] configContent = ['''%s''']
def configSb = new StringBuffer()
for (int i=0; i<configContent.size(); i++) {
configSb << configContent[i]
}
def stream = new ByteArrayInputStream(configSb.toString().getBytes('UTF-8'))
def source = new io.jenkins.plugins.casc.yaml.YamlSource(stream, io.jenkins.plugins.casc.yaml.YamlSource.READ_FROM_INPUTSTREAM)
io.jenkins.plugins.casc.ConfigurationAsCode.get().configureWith(source)
`
func prepareScript(script string) string {
var slicedScript []string
if len(script) > groovyUtf8MaxStringLength {
slicedScript = splitTooLongScript(script)
} else {
slicedScript = append(slicedScript, script)
}
return strings.Join(slicedScript, "''','''")
}
func splitTooLongScript(groovyScript string) []string {
var slicedGroovyScript []string
lastSubstrIndex := len(groovyScript) % groovyUtf8MaxStringLength
lastSubstr := groovyScript[len(groovyScript)-lastSubstrIndex:]
substrNumber := len(groovyScript) / groovyUtf8MaxStringLength
for i := 0; i < substrNumber; i++ {
scriptSubstr := groovyScript[i*groovyUtf8MaxStringLength : (i*groovyUtf8MaxStringLength)+groovyUtf8MaxStringLength]
slicedGroovyScript = append(slicedGroovyScript, scriptSubstr)
}
slicedGroovyScript = append(slicedGroovyScript, lastSubstr)
return slicedGroovyScript
}
File diff suppressed because one or more lines are too long
@@ -1,2 +0,0 @@
// Package casc configures Jenkins with help Configuration as a code plugin
package casc
@@ -1,2 +0,0 @@
// Package user implements Jenkins user configuration and reconciliation
package user
@@ -1,122 +0,0 @@
package user
import (
"strings"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
"github.com/go-logr/logr"
jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/backuprestore"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/user/casc"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/user/seedjobs"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/groovy"
"github.com/jenkinsci/kubernetes-operator/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
// ReconcileUserConfiguration defines API client for reconcile User Configuration
type ReconcileUserConfiguration interface {
ReconcileCasc() (reconcile.Result, error)
ReconcileOthers() (reconcile.Result, error)
Validate(jenkins *v1alpha2.Jenkins) ([]string, error)
}
type reconcileUserConfiguration struct {
configuration.Configuration
jenkinsClient jenkinsclient.Jenkins
logger logr.Logger
}
// New create structure which takes care of user configuration.
func New(configuration configuration.Configuration, jenkinsClient jenkinsclient.Jenkins) ReconcileUserConfiguration {
return &reconcileUserConfiguration{
Configuration: configuration,
jenkinsClient: jenkinsClient,
logger: log.Log.WithValues("cr", configuration.Jenkins.Name),
}
}
// ReconcileCasc is a reconcile loop for casc.
func (r *reconcileUserConfiguration) ReconcileCasc() (reconcile.Result, error) {
result, err := r.ensureCasc(r.jenkinsClient)
if err != nil {
return reconcile.Result{}, err
}
if result.Requeue {
return result, nil
}
return reconcile.Result{}, nil
}
// Reconcile it's a main reconciliation loop for user supplied configuration
func (r *reconcileUserConfiguration) ReconcileOthers() (reconcile.Result, error) {
backupAndRestore := backuprestore.New(r.Configuration, r.logger)
result, err := r.ensureSeedJobs()
if err != nil {
return reconcile.Result{}, err
}
if result.Requeue {
return result, nil
}
if err := backupAndRestore.Restore(r.jenkinsClient); err != nil {
return reconcile.Result{}, err
}
if err := backupAndRestore.Backup(false); err != nil {
return reconcile.Result{}, err
}
if err := backupAndRestore.EnsureBackupTrigger(); err != nil {
return reconcile.Result{}, err
}
return reconcile.Result{}, nil
}
func (r *reconcileUserConfiguration) ensureSeedJobs() (reconcile.Result, error) {
seedJobs := seedjobs.New(r.jenkinsClient, r.Configuration)
done, err := seedJobs.EnsureSeedJobs(r.Configuration.Jenkins)
if err != nil {
return reconcile.Result{}, err
}
if !done {
return reconcile.Result{Requeue: true}, nil
}
return reconcile.Result{}, nil
}
func (r *reconcileUserConfiguration) ensureCasc(jenkinsClient jenkinsclient.Jenkins) (reconcile.Result, error) {
configurationAsCodeClient := casc.New(jenkinsClient, r.Client, r.Configuration.Jenkins)
requeue, err := configurationAsCodeClient.Ensure(r.Configuration.Jenkins)
if err != nil {
return reconcile.Result{}, err
}
if requeue {
return reconcile.Result{Requeue: true}, nil
}
groovyClient := groovy.New(jenkinsClient, r.Client, r.Configuration.Jenkins, "user-groovy", r.Configuration.Jenkins.Spec.GroovyScripts.Customization)
requeue, err = groovyClient.WaitForSecretSynchronization(resources.GroovyScriptsSecretVolumePath)
if err != nil {
return reconcile.Result{}, err
}
if requeue {
return reconcile.Result{Requeue: true}, nil
}
requeue, err = groovyClient.Ensure(func(name string) bool {
return strings.HasSuffix(name, ".groovy")
}, groovy.AddSecretsLoaderToGroovyScript(resources.GroovyScriptsSecretVolumePath))
if err != nil {
return reconcile.Result{}, err
}
if requeue {
return reconcile.Result{Requeue: true}, nil
}
return reconcile.Result{}, nil
}
@@ -1,2 +0,0 @@
// Package seedjobs implements seed jobs configuration
package seedjobs
@@ -1,539 +0,0 @@
package seedjobs
import (
"context"
"crypto/sha256"
"encoding/base64"
"fmt"
"reflect"
"text/template"
"github.com/go-logr/logr"
"github.com/jenkinsci/kubernetes-operator/internal/render"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/groovy"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/reason"
"github.com/jenkinsci/kubernetes-operator/pkg/log"
stackerr "github.com/pkg/errors"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)
const (
// UsernameSecretKey is username data key in Kubernetes secret used to create Jenkins username/password credential
UsernameSecretKey = "username"
// PasswordSecretKey is password data key in Kubernetes secret used to create Jenkins username/password credential
PasswordSecretKey = "password"
// PrivateKeySecretKey is private key data key in Kubernetes secret used to create Jenkins SSH credential
PrivateKeySecretKey = "privateKey"
// JenkinsCredentialTypeLabelName is label for kubernetes-credentials-provider-plugin which determine Jenkins
// credential type
JenkinsCredentialTypeLabelName = "jenkins.io/credentials-type"
// AgentName is the name of seed job agent
AgentName = "seed-job-agent"
creatingGroovyScriptName = "seed-job-groovy-script.groovy"
homeVolumeName = "home"
homeVolumePath = "/home/jenkins/agent"
workspaceVolumeName = "workspace"
workspaceVolumePath = "/home/jenkins/workspace"
)
var seedJobGroovyScriptTemplate = template.Must(template.New(creatingGroovyScriptName).Parse(`
import hudson.model.FreeStyleProject;
import hudson.plugins.git.GitSCM;
import hudson.plugins.git.BranchSpec;
import hudson.triggers.SCMTrigger;
import hudson.triggers.TimerTrigger;
import hudson.util.Secret;
import javaposse.jobdsl.plugin.*;
import jenkins.model.Jenkins;
import jenkins.model.JenkinsLocationConfiguration;
import com.cloudbees.plugins.credentials.CredentialsScope;
import com.cloudbees.plugins.credentials.domains.Domain;
import com.cloudbees.plugins.credentials.SystemCredentialsProvider;
import jenkins.model.JenkinsLocationConfiguration;
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
import org.jenkinsci.plugins.workflow.cps.CpsScmFlowDefinition;
{{ if .GitHubPushTrigger }}
import com.cloudbees.jenkins.GitHubPushTrigger;
{{ end }}
{{ if .BitbucketPushTrigger }}
import com.cloudbees.jenkins.plugins.BitBucketTrigger;
{{ end }}
import hudson.model.FreeStyleProject;
import hudson.model.labels.LabelAtom;
import hudson.plugins.git.BranchSpec;
import hudson.plugins.git.GitSCM;
import hudson.plugins.git.SubmoduleConfig;
import hudson.plugins.git.extensions.impl.CloneOption;
import javaposse.jobdsl.plugin.ExecuteDslScripts;
import javaposse.jobdsl.plugin.LookupStrategy;
import javaposse.jobdsl.plugin.RemovedJobAction;
import javaposse.jobdsl.plugin.RemovedViewAction;
import static com.google.common.collect.Lists.newArrayList;
Jenkins jenkins = Jenkins.instance
def jobDslSeedName = "{{ .ID }}-{{ .SeedJobSuffix }}";
def jobRef = jenkins.getItem(jobDslSeedName)
def repoList = GitSCM.createRepoList("{{ .RepositoryURL }}", "{{ .CredentialID }}")
def gitExtensions = [new CloneOption(true, true, ";", 10)]
def scm = new GitSCM(
repoList,
newArrayList(new BranchSpec("{{ .RepositoryBranch }}")),
false,
Collections.<SubmoduleConfig>emptyList(),
null,
null,
gitExtensions
)
def executeDslScripts = new ExecuteDslScripts()
executeDslScripts.setTargets("{{ .Targets }}")
executeDslScripts.setSandbox(false)
executeDslScripts.setRemovedJobAction(RemovedJobAction.DELETE)
executeDslScripts.setRemovedViewAction(RemovedViewAction.DELETE)
executeDslScripts.setLookupStrategy(LookupStrategy.SEED_JOB)
executeDslScripts.setAdditionalClasspath("{{ .AdditionalClasspath }}")
executeDslScripts.setFailOnMissingPlugin({{ .FailOnMissingPlugin }})
executeDslScripts.setUnstableOnDeprecation({{ .UnstableOnDeprecation }})
executeDslScripts.setIgnoreMissingFiles({{ .IgnoreMissingFiles }})
if (jobRef == null) {
jobRef = jenkins.createProject(FreeStyleProject, jobDslSeedName)
}
jobRef.getBuildersList().clear()
jobRef.getBuildersList().add(executeDslScripts)
jobRef.setDisplayName("Seed Job from {{ .ID }}")
jobRef.setScm(scm)
{{ if .PollSCM }}
jobRef.addTrigger(new SCMTrigger("{{ .PollSCM }}"))
{{ end }}
{{ if .GitHubPushTrigger }}
jobRef.addTrigger(new GitHubPushTrigger())
{{ end }}
{{ if .BitbucketPushTrigger }}
jobRef.addTrigger(new BitBucketTrigger())
{{ end }}
{{ if .BuildPeriodically }}
jobRef.addTrigger(new TimerTrigger("{{ .BuildPeriodically }}"))
{{ end}}
jobRef.setAssignedLabel(new LabelAtom("{{ .AgentName }}"))
jenkins.getQueue().schedule(jobRef)
`))
// SeedJobs defines client interface to SeedJobs
type SeedJobs interface {
EnsureSeedJobs(jenkins *v1alpha2.Jenkins) (done bool, err error)
waitForSeedJobAgent(agentName string) (requeue bool, err error)
createJobs(jenkins *v1alpha2.Jenkins) (requeue bool, err error)
ensureLabelsForSecrets(jenkins v1alpha2.Jenkins) error
credentialValue(namespace string, seedJob v1alpha2.SeedJob) (string, error)
getAllSeedJobIDs(jenkins v1alpha2.Jenkins) []string
isRecreatePodNeeded(jenkins v1alpha2.Jenkins) bool
createAgent(jenkinsClient jenkinsclient.Jenkins, k8sClient client.Client, jenkinsManifest *v1alpha2.Jenkins, namespace string, agentName string) error
ValidateSeedJobs(jenkins v1alpha2.Jenkins) ([]string, error)
validateSchedule(job v1alpha2.SeedJob, str string, key string) []string
validateGitHubPushTrigger(jenkins v1alpha2.Jenkins) []string
validateBitbucketPushTrigger(jenkins v1alpha2.Jenkins) []string
validateIfIDIsUnique(seedJobs []v1alpha2.SeedJob) []string
}
type seedJobs struct {
configuration.Configuration
jenkinsClient jenkinsclient.Jenkins
logger logr.Logger
}
// New creates SeedJobs object
func New(jenkinsClient jenkinsclient.Jenkins, config configuration.Configuration) SeedJobs {
return &seedJobs{
Configuration: config,
jenkinsClient: jenkinsClient,
logger: log.Log.WithValues("cr", config.Jenkins.Name),
}
}
// EnsureSeedJobs configures seed job and runs it for every entry from Jenkins.Spec.SeedJobs
func (s *seedJobs) EnsureSeedJobs(jenkins *v1alpha2.Jenkins) (done bool, err error) {
if s.isRecreatePodNeeded(*jenkins) {
message := "Some seed job has been deleted, recreating pod"
s.logger.Info(message)
restartReason := reason.NewPodRestart(
reason.OperatorSource,
[]string{message},
)
return false, s.RestartJenkinsMasterPod(restartReason)
}
if len(jenkins.Spec.SeedJobs) > 0 {
err := s.createAgent(s.jenkinsClient, s.Client, jenkins, jenkins.Namespace, AgentName)
if err != nil {
return false, err
}
requeue, err := s.waitForSeedJobAgent(AgentName)
if err != nil {
return false, err
}
if requeue {
return false, nil
}
} else if len(jenkins.Spec.SeedJobs) == 0 {
err := s.Client.Delete(context.TODO(), &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Namespace: jenkins.Namespace,
Name: agentDeploymentName(*jenkins, AgentName),
},
})
if err != nil && !apierrors.IsNotFound(err) {
return false, stackerr.WithStack(err)
}
}
if err = s.ensureLabelsForSecrets(*jenkins); err != nil {
return false, err
}
requeue, err := s.createJobs(jenkins)
if err != nil {
return false, err
}
if requeue {
return false, nil
}
seedJobIDs := s.getAllSeedJobIDs(*jenkins)
if !reflect.DeepEqual(seedJobIDs, jenkins.Status.CreatedSeedJobs) {
jenkins.Status.CreatedSeedJobs = seedJobIDs
return false, stackerr.WithStack(s.Client.Update(context.TODO(), jenkins))
}
return true, nil
}
func (s *seedJobs) waitForSeedJobAgent(agentName string) (requeue bool, err error) {
agent := appsv1.Deployment{}
err = s.Client.Get(context.TODO(), types.NamespacedName{Name: agentDeploymentName(*s.Jenkins, agentName), Namespace: s.Jenkins.Namespace}, &agent)
if apierrors.IsNotFound(err) {
return true, nil
} else if err != nil {
return true, err
}
noReadyReplicas := agent.Status.ReadyReplicas == 0
if noReadyReplicas {
s.logger.Info(fmt.Sprintf("Waiting for Seed Job Agent `%s`...", agentName))
return true, nil
}
return false, nil
}
// createJob is responsible for creating jenkins job which configures jenkins seed jobs and deploy keys
func (s *seedJobs) createJobs(jenkins *v1alpha2.Jenkins) (requeue bool, err error) {
groovyClient := groovy.New(s.jenkinsClient, s.Client, jenkins, "seed-jobs", jenkins.Spec.GroovyScripts.Customization)
for _, seedJob := range jenkins.Spec.SeedJobs {
credentialValue, err := s.credentialValue(jenkins.Namespace, seedJob)
if err != nil {
return true, err
}
groovyScript, err := seedJobCreatingGroovyScript(seedJob)
if err != nil {
return true, err
}
hash := sha256.New()
hash.Write([]byte(groovyScript))
hash.Write([]byte(credentialValue))
requeue, err := groovyClient.EnsureSingle(seedJob.ID, fmt.Sprintf("%s.groovy", seedJob.ID), base64.URLEncoding.EncodeToString(hash.Sum(nil)), groovyScript)
if err != nil {
return true, err
}
if requeue {
return true, nil
}
}
return false, nil
}
// ensureLabelsForSecrets adds labels to Kubernetes secrets where are Jenkins credentials used for seed jobs,
// thanks to them kubernetes-credentials-provider-plugin will create Jenkins credentials in Jenkins and
// Operator will able to watch any changes made to them
func (s *seedJobs) ensureLabelsForSecrets(jenkins v1alpha2.Jenkins) error {
for _, seedJob := range jenkins.Spec.SeedJobs {
if seedJob.JenkinsCredentialType == v1alpha2.BasicSSHCredentialType || seedJob.JenkinsCredentialType == v1alpha2.UsernamePasswordCredentialType {
requiredLabels := resources.BuildLabelsForWatchedResources(jenkins)
requiredLabels[JenkinsCredentialTypeLabelName] = string(seedJob.JenkinsCredentialType)
secret := &corev1.Secret{}
namespaceName := types.NamespacedName{Namespace: jenkins.ObjectMeta.Namespace, Name: seedJob.CredentialID}
err := s.Client.Get(context.TODO(), namespaceName, secret)
if err != nil {
return stackerr.WithStack(err)
}
if !resources.VerifyIfLabelsAreSet(secret, requiredLabels) {
secret.ObjectMeta.Labels = requiredLabels
if err = s.Client.Update(context.TODO(), secret); err != nil {
return stackerr.WithStack(err)
}
}
}
}
return nil
}
func (s *seedJobs) credentialValue(namespace string, seedJob v1alpha2.SeedJob) (string, error) {
if seedJob.JenkinsCredentialType == v1alpha2.BasicSSHCredentialType || seedJob.JenkinsCredentialType == v1alpha2.UsernamePasswordCredentialType {
secret := &corev1.Secret{}
namespaceName := types.NamespacedName{Namespace: namespace, Name: seedJob.CredentialID}
err := s.Client.Get(context.TODO(), namespaceName, secret)
if err != nil {
return "", stackerr.WithStack(err)
}
if seedJob.JenkinsCredentialType == v1alpha2.BasicSSHCredentialType {
return string(secret.Data[PrivateKeySecretKey]), nil
}
return string(secret.Data[UsernameSecretKey]) + string(secret.Data[PasswordSecretKey]), nil
}
return "", nil
}
func (s *seedJobs) getAllSeedJobIDs(jenkins v1alpha2.Jenkins) []string {
var ids []string
for _, seedJob := range jenkins.Spec.SeedJobs {
ids = append(ids, seedJob.ID)
}
return ids
}
func (s *seedJobs) isRecreatePodNeeded(jenkins v1alpha2.Jenkins) bool {
for _, createdSeedJob := range jenkins.Status.CreatedSeedJobs {
found := false
for _, seedJob := range jenkins.Spec.SeedJobs {
if createdSeedJob == seedJob.ID {
found = true
break
}
}
if !found {
return true
}
}
return false
}
// createAgent deploys Jenkins agent to Kubernetes cluster
func (s *seedJobs) createAgent(jenkinsClient jenkinsclient.Jenkins, k8sClient client.Client, jenkinsManifest *v1alpha2.Jenkins, namespace string, agentName string) error {
_, err := jenkinsClient.GetNode(agentName)
// Create node if not exists
if err != nil && err.Error() == "No node found" {
_, err = jenkinsClient.CreateNode(agentName, 5, "The jenkins-operator generated agent", "/home/jenkins", agentName)
if err != nil {
return stackerr.WithStack(err)
}
} else if err != nil {
return stackerr.WithStack(err)
}
secret, err := jenkinsClient.GetNodeSecret(agentName)
if err != nil {
return err
}
deployment, err := agentDeployment(jenkinsManifest, namespace, agentName, secret)
if err != nil {
return err
}
err = k8sClient.Create(context.TODO(), deployment)
if apierrors.IsAlreadyExists(err) {
err := k8sClient.Update(context.TODO(), deployment)
if err != nil {
return stackerr.WithStack(err)
}
} else if err != nil {
return stackerr.WithStack(err)
}
return nil
}
func agentDeploymentName(jenkins v1alpha2.Jenkins, agentName string) string {
return fmt.Sprintf("%s-%s", agentName, jenkins.Name)
}
func agentDeployment(jenkins *v1alpha2.Jenkins, namespace string, agentName string, secret string) (*appsv1.Deployment, error) {
jenkinsSlavesServiceFQDN, err := resources.GetJenkinsSlavesServiceFQDN(jenkins)
if err != nil {
return nil, err
}
jenkinsHTTPServiceFQDN, err := resources.GetJenkinsHTTPServiceFQDN(jenkins)
if err != nil {
return nil, err
}
return &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: agentDeploymentName(*jenkins, agentName),
Namespace: namespace,
OwnerReferences: []metav1.OwnerReference{
{
BlockOwnerDeletion: &[]bool{true}[0],
Controller: &[]bool{true}[0],
Kind: jenkins.Kind,
Name: jenkins.Name,
APIVersion: jenkins.APIVersion,
UID: jenkins.UID,
},
},
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
NodeSelector: jenkins.Spec.Master.NodeSelector,
Tolerations: jenkins.Spec.Master.Tolerations,
ImagePullSecrets: jenkins.Spec.Master.ImagePullSecrets,
Containers: []corev1.Container{
{
Name: "jnlp",
Image: "jenkins/inbound-agent:alpine",
Env: []corev1.EnvVar{
{
Name: "JENKINS_TUNNEL",
Value: fmt.Sprintf("%s:%d",
jenkinsSlavesServiceFQDN,
jenkins.Spec.SlaveService.Port),
},
{
Name: "JENKINS_SECRET",
Value: secret,
},
{
Name: "JENKINS_AGENT_NAME",
Value: agentName,
},
{
Name: "JENKINS_URL",
Value: fmt.Sprintf("http://%s:%d",
jenkinsHTTPServiceFQDN,
jenkins.Spec.Service.Port,
),
},
{
Name: "JENKINS_AGENT_WORKDIR",
Value: homeVolumePath,
},
},
VolumeMounts: []corev1.VolumeMount{
{
Name: homeVolumeName,
MountPath: homeVolumePath,
},
{
Name: workspaceVolumeName,
MountPath: workspaceVolumePath,
},
},
},
},
Volumes: []corev1.Volume{
{
Name: homeVolumeName,
VolumeSource: corev1.VolumeSource{
EmptyDir: &corev1.EmptyDirVolumeSource{},
},
},
{
Name: workspaceVolumeName,
VolumeSource: corev1.VolumeSource{
EmptyDir: &corev1.EmptyDirVolumeSource{},
},
},
},
},
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app": fmt.Sprintf("%s-selector", agentName),
},
},
},
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": fmt.Sprintf("%s-selector", agentName),
},
},
},
}, nil
}
func seedJobCreatingGroovyScript(seedJob v1alpha2.SeedJob) (string, error) {
data := struct {
ID string
CredentialID string
Targets string
RepositoryBranch string
RepositoryURL string
BitbucketPushTrigger bool
GitHubPushTrigger bool
BuildPeriodically string
PollSCM string
IgnoreMissingFiles bool
AdditionalClasspath string
FailOnMissingPlugin bool
UnstableOnDeprecation bool
SeedJobSuffix string
AgentName string
}{
ID: seedJob.ID,
CredentialID: seedJob.CredentialID,
Targets: seedJob.Targets,
RepositoryBranch: seedJob.RepositoryBranch,
RepositoryURL: seedJob.RepositoryURL,
BitbucketPushTrigger: seedJob.BitbucketPushTrigger,
GitHubPushTrigger: seedJob.GitHubPushTrigger,
BuildPeriodically: seedJob.BuildPeriodically,
PollSCM: seedJob.PollSCM,
IgnoreMissingFiles: seedJob.IgnoreMissingFiles,
AdditionalClasspath: seedJob.AdditionalClasspath,
FailOnMissingPlugin: seedJob.FailOnMissingPlugin,
UnstableOnDeprecation: seedJob.UnstableOnDeprecation,
SeedJobSuffix: constants.SeedJobSuffix,
AgentName: AgentName,
}
output, err := render.Render(seedJobGroovyScriptTemplate, data)
if err != nil {
return "", err
}
return output, nil
}
@@ -1,282 +0,0 @@
package seedjobs
import (
"context"
"testing"
"k8s.io/apimachinery/pkg/api/errors"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources"
"github.com/bndr/gojenkins"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
const agentSecret = "test-secret"
func jenkinsCustomResource() *v1alpha2.Jenkins {
return &v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Name: "jenkins",
Namespace: "default",
},
Spec: v1alpha2.JenkinsSpec{
Master: v1alpha2.JenkinsMaster{
Annotations: map[string]string{"test": "label"},
Containers: []v1alpha2.Container{
{
Name: resources.JenkinsMasterContainerName,
Image: "jenkins/jenkins",
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("300m"),
corev1.ResourceMemory: resource.MustParse("500Mi"),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("2"),
corev1.ResourceMemory: resource.MustParse("2Gi"),
},
},
},
},
},
SeedJobs: []v1alpha2.SeedJob{
{
ID: "jenkins-operator-e2e",
JenkinsCredentialType: v1alpha2.NoJenkinsCredentialCredentialType,
Targets: "cicd/jobs/*.jenkins",
Description: "Jenkins Operator e2e tests repository",
RepositoryBranch: "master",
RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git",
},
},
},
}
}
func TestEnsureSeedJobs(t *testing.T) {
t.Run("happy", func(t *testing.T) {
// given
ctrl := gomock.NewController(t)
ctx := context.TODO()
defer ctrl.Finish()
jenkinsClient := jenkinsclient.NewMockJenkins(ctrl)
fakeClient := fake.NewFakeClient()
err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme)
assert.NoError(t, err)
jenkins := jenkinsCustomResource()
err = fakeClient.Create(ctx, jenkins)
assert.NoError(t, err)
testNode := &gojenkins.Node{
Raw: &gojenkins.NodeResponse{
DisplayName: AgentName,
},
}
config := configuration.Configuration{
Client: fakeClient,
ClientSet: kubernetes.Clientset{},
Notifications: nil,
Jenkins: jenkins,
}
seedJobCreatingScript, err := seedJobCreatingGroovyScript(jenkins.Spec.SeedJobs[0])
assert.NoError(t, err)
jenkinsClient.EXPECT().GetNode(AgentName).Return(nil, nil).AnyTimes()
jenkinsClient.EXPECT().CreateNode(AgentName, 1, "The jenkins-operator generated agent", "/home/jenkins", AgentName).Return(testNode, nil).AnyTimes()
jenkinsClient.EXPECT().GetNodeSecret(AgentName).Return(agentSecret, nil).AnyTimes()
jenkinsClient.EXPECT().ExecuteScript(seedJobCreatingScript).AnyTimes()
seedJobClient := New(jenkinsClient, config)
// when
_, err = seedJobClient.EnsureSeedJobs(jenkins)
// then
assert.NoError(t, err)
var agentDeployment appsv1.Deployment
err = fakeClient.Get(ctx, types.NamespacedName{Namespace: jenkins.Namespace, Name: agentDeploymentName(*jenkins, AgentName)}, &agentDeployment)
assert.NoError(t, err)
})
t.Run("delete agent deployment when no seed jobs", func(t *testing.T) {
// given
ctrl := gomock.NewController(t)
ctx := context.TODO()
defer ctrl.Finish()
jenkins := jenkinsCustomResource()
jenkins.Spec.SeedJobs = []v1alpha2.SeedJob{}
jenkinsClient := jenkinsclient.NewMockJenkins(ctrl)
fakeClient := fake.NewFakeClient()
err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme)
assert.NoError(t, err)
config := configuration.Configuration{
Client: fakeClient,
ClientSet: kubernetes.Clientset{},
Notifications: nil,
Jenkins: jenkins,
}
jenkinsClient.EXPECT().GetNode(AgentName).AnyTimes()
jenkinsClient.EXPECT().CreateNode(AgentName, 1, "The jenkins-operator generated agent", "/home/jenkins", AgentName).AnyTimes()
jenkinsClient.EXPECT().GetNodeSecret(AgentName).Return(agentSecret, nil).AnyTimes()
seedJobsClient := New(jenkinsClient, config)
err = fakeClient.Create(ctx, &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: agentDeploymentName(*jenkins, AgentName),
Namespace: jenkins.Namespace,
},
})
assert.NoError(t, err)
// when
_, err = seedJobsClient.EnsureSeedJobs(jenkins)
// then
assert.NoError(t, err)
var deployment appsv1.Deployment
err = fakeClient.Get(ctx, types.NamespacedName{Namespace: jenkins.Namespace, Name: agentDeploymentName(*jenkins, AgentName)}, &deployment)
assert.True(t, errors.IsNotFound(err), "Agent deployment hasn't been deleted")
})
}
func TestCreateAgent(t *testing.T) {
t.Run("don't fail when deployment is already created", func(t *testing.T) {
// given
ctrl := gomock.NewController(t)
ctx := context.TODO()
defer ctrl.Finish()
jenkins := jenkinsCustomResource()
jenkinsClient := jenkinsclient.NewMockJenkins(ctrl)
fakeClient := fake.NewFakeClient()
err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme)
assert.NoError(t, err)
jenkinsClient.EXPECT().GetNode(AgentName).AnyTimes()
jenkinsClient.EXPECT().CreateNode(AgentName, 1, "The jenkins-operator generated agent", "/home/jenkins", AgentName).AnyTimes()
jenkinsClient.EXPECT().GetNodeSecret(AgentName).Return(agentSecret, nil).AnyTimes()
config := configuration.Configuration{
Client: fakeClient,
ClientSet: kubernetes.Clientset{},
Notifications: nil,
Jenkins: &v1alpha2.Jenkins{},
}
seedJobsClient := New(jenkinsClient, config)
err = fakeClient.Create(ctx, &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: agentDeploymentName(*jenkins, AgentName),
Namespace: jenkins.Namespace,
},
})
assert.NoError(t, err)
// when
err = seedJobsClient.createAgent(jenkinsClient, fakeClient, jenkinsCustomResource(), jenkins.Namespace, AgentName)
// then
assert.NoError(t, err)
})
}
func TestSeedJobs_isRecreatePodNeeded(t *testing.T) {
config := configuration.Configuration{
Client: nil,
ClientSet: kubernetes.Clientset{},
Notifications: nil,
Jenkins: &v1alpha2.Jenkins{},
}
seedJobsClient := New(nil, config)
t.Run("empty", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{}
got := seedJobsClient.isRecreatePodNeeded(jenkins)
assert.False(t, got)
})
t.Run("same", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
SeedJobs: []v1alpha2.SeedJob{
{
ID: "name",
},
},
},
Status: v1alpha2.JenkinsStatus{
CreatedSeedJobs: []string{"name"},
},
}
got := seedJobsClient.isRecreatePodNeeded(jenkins)
assert.False(t, got)
})
t.Run("removed one", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
SeedJobs: []v1alpha2.SeedJob{
{
ID: "name1",
},
},
},
Status: v1alpha2.JenkinsStatus{
CreatedSeedJobs: []string{"name1", "name2"},
},
}
got := seedJobsClient.isRecreatePodNeeded(jenkins)
assert.True(t, got)
})
t.Run("renamed one", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
SeedJobs: []v1alpha2.SeedJob{
{
ID: "name1",
},
{
ID: "name3",
},
},
},
Status: v1alpha2.JenkinsStatus{
CreatedSeedJobs: []string{"name1", "name2"},
},
}
got := seedJobsClient.isRecreatePodNeeded(jenkins)
assert.True(t, got)
})
}
@@ -1,240 +0,0 @@
package seedjobs
import (
"context"
"crypto/x509"
"encoding/pem"
"fmt"
"strings"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
stackerr "github.com/pkg/errors"
"github.com/robfig/cron"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
)
// ValidateSeedJobs verify seed jobs configuration
func (s *seedJobs) ValidateSeedJobs(jenkins v1alpha2.Jenkins) ([]string, error) {
var messages []string
if msg := s.validateIfIDIsUnique(jenkins.Spec.SeedJobs); len(msg) > 0 {
messages = append(messages, msg...)
}
for _, seedJob := range jenkins.Spec.SeedJobs {
if len(seedJob.ID) == 0 {
messages = append(messages, fmt.Sprintf("seedJob `%s` id can't be empty", seedJob.ID))
}
if len(seedJob.RepositoryBranch) == 0 {
messages = append(messages, fmt.Sprintf("seedJob `%s` repository branch can't be empty", seedJob.ID))
}
if len(seedJob.RepositoryURL) == 0 {
messages = append(messages, fmt.Sprintf("seedJob `%s` repository URL branch can't be empty", seedJob.ID))
}
if len(seedJob.Targets) == 0 {
messages = append(messages, fmt.Sprintf("seedJob `%s` targets can't be empty", seedJob.ID))
}
if _, ok := v1alpha2.AllowedJenkinsCredentialMap[string(seedJob.JenkinsCredentialType)]; !ok {
messages = append(messages, fmt.Sprintf("seedJob `%s` unknown credential type", seedJob.ID))
}
if (seedJob.JenkinsCredentialType == v1alpha2.BasicSSHCredentialType ||
seedJob.JenkinsCredentialType == v1alpha2.UsernamePasswordCredentialType) && len(seedJob.CredentialID) == 0 {
messages = append(messages, fmt.Sprintf("seedJob `%s` credential ID can't be empty", seedJob.ID))
}
// validate repository url match private key
if strings.Contains(seedJob.RepositoryURL, "git@") && seedJob.JenkinsCredentialType == v1alpha2.NoJenkinsCredentialCredentialType {
messages = append(messages, fmt.Sprintf("seedJob `%s` Jenkins credential must be set while using ssh repository url", seedJob.ID))
}
if seedJob.JenkinsCredentialType == v1alpha2.BasicSSHCredentialType ||
seedJob.JenkinsCredentialType == v1alpha2.UsernamePasswordCredentialType ||
seedJob.JenkinsCredentialType == v1alpha2.ExternalCredentialType {
secret := &v1.Secret{}
namespaceName := types.NamespacedName{Namespace: jenkins.Namespace, Name: seedJob.CredentialID}
err := s.Client.Get(context.TODO(), namespaceName, secret)
if err != nil && apierrors.IsNotFound(err) {
messages = append(messages, fmt.Sprintf("seedJob `%s` required secret '%s' with Jenkins credential not found", seedJob.ID, seedJob.CredentialID))
} else if err != nil {
return nil, stackerr.WithStack(err)
}
if seedJob.JenkinsCredentialType == v1alpha2.BasicSSHCredentialType {
if msg := validateBasicSSHSecret(*secret); len(msg) > 0 {
for _, m := range msg {
messages = append(messages, fmt.Sprintf("seedJob `%s` %s", seedJob.ID, m))
}
}
}
if seedJob.JenkinsCredentialType == v1alpha2.UsernamePasswordCredentialType {
if msg := validateUsernamePasswordSecret(*secret); len(msg) > 0 {
for _, m := range msg {
messages = append(messages, fmt.Sprintf("seedJob `%s` %s", seedJob.ID, m))
}
}
}
}
if len(seedJob.BuildPeriodically) > 0 {
if msg := s.validateSchedule(seedJob, seedJob.BuildPeriodically, "buildPeriodically"); len(msg) > 0 {
for _, m := range msg {
messages = append(messages, fmt.Sprintf("seedJob `%s` %s", seedJob.ID, m))
}
}
}
if len(seedJob.PollSCM) > 0 {
if msg := s.validateSchedule(seedJob, seedJob.PollSCM, "pollSCM"); len(msg) > 0 {
for _, m := range msg {
messages = append(messages, fmt.Sprintf("seedJob `%s` %s", seedJob.ID, m))
}
}
}
if seedJob.GitHubPushTrigger {
if msg := s.validateGitHubPushTrigger(jenkins); len(msg) > 0 {
for _, m := range msg {
messages = append(messages, fmt.Sprintf("seedJob `%s` %s", seedJob.ID, m))
}
}
}
if seedJob.BitbucketPushTrigger {
if msg := s.validateBitbucketPushTrigger(jenkins); len(msg) > 0 {
for _, m := range msg {
messages = append(messages, fmt.Sprintf("seedJob `%s` %s", seedJob.ID, m))
}
}
}
}
return messages, nil
}
func (s *seedJobs) validateSchedule(job v1alpha2.SeedJob, str string, key string) []string {
var messages []string
_, err := cron.Parse(str)
if err != nil {
messages = append(messages, fmt.Sprintf("`%s` schedule '%s' is invalid cron spec in `%s`", key, str, job.ID))
}
return messages
}
func (s *seedJobs) validateGitHubPushTrigger(jenkins v1alpha2.Jenkins) []string {
var messages []string
if err := checkPluginExists(jenkins, "github"); err != nil {
return append(messages, fmt.Sprintf("githubPushTrigger cannot be enabled: %s", err))
}
return messages
}
func (s *seedJobs) validateBitbucketPushTrigger(jenkins v1alpha2.Jenkins) []string {
var messages []string
if err := checkPluginExists(jenkins, "bitbucket"); err != nil {
return append(messages, fmt.Sprintf("bitbucketPushTrigger cannot be enabled: %s", err))
}
return messages
}
func checkPluginExists(jenkins v1alpha2.Jenkins, name string) error {
exists := false
for _, plugin := range jenkins.Spec.Master.BasePlugins {
if plugin.Name == name {
exists = true
}
}
userExists := false
for _, plugin := range jenkins.Spec.Master.Plugins {
if plugin.Name == name {
userExists = true
}
}
if !exists && !userExists {
return fmt.Errorf("`%s` plugin not installed", name)
}
return nil
}
func (s *seedJobs) validateIfIDIsUnique(seedJobs []v1alpha2.SeedJob) []string {
var messages []string
ids := map[string]bool{}
for _, seedJob := range seedJobs {
if _, found := ids[seedJob.ID]; found {
messages = append(messages, fmt.Sprintf("'%s' seed job ID is not unique", seedJob.ID))
}
ids[seedJob.ID] = true
}
return messages
}
func validateBasicSSHSecret(secret v1.Secret) []string {
var messages []string
username, exists := secret.Data[UsernameSecretKey]
if !exists {
messages = append(messages, fmt.Sprintf("required data '%s' not found in secret '%s'", UsernameSecretKey, secret.ObjectMeta.Name))
}
if len(username) == 0 {
messages = append(messages, fmt.Sprintf("required data '%s' is empty in secret '%s'", UsernameSecretKey, secret.ObjectMeta.Name))
}
privateKey, exists := secret.Data[PrivateKeySecretKey]
if !exists {
messages = append(messages, fmt.Sprintf("required data '%s' not found in secret '%s'", PrivateKeySecretKey, secret.ObjectMeta.Name))
}
if len(string(privateKey)) == 0 {
messages = append(messages, fmt.Sprintf("required data '%s' not found in secret '%s'", PrivateKeySecretKey, secret.ObjectMeta.Name))
}
if err := validatePrivateKey(string(privateKey)); err != nil {
messages = append(messages, fmt.Sprintf("private key '%s' invalid in secret '%s': %s", PrivateKeySecretKey, secret.ObjectMeta.Name, err))
}
return messages
}
func validateUsernamePasswordSecret(secret v1.Secret) []string {
var messages []string
username, exists := secret.Data[UsernameSecretKey]
if !exists {
messages = append(messages, fmt.Sprintf("required data '%s' not found in secret '%s'", UsernameSecretKey, secret.ObjectMeta.Name))
}
if len(username) == 0 {
messages = append(messages, fmt.Sprintf("required data '%s' is empty in secret '%s'", UsernameSecretKey, secret.ObjectMeta.Name))
}
password, exists := secret.Data[PasswordSecretKey]
if !exists {
messages = append(messages, fmt.Sprintf("required data '%s' not found in secret '%s'", PasswordSecretKey, secret.ObjectMeta.Name))
}
if len(password) == 0 {
messages = append(messages, fmt.Sprintf("required data '%s' is empty in secret '%s'", PasswordSecretKey, secret.ObjectMeta.Name))
}
return messages
}
func validatePrivateKey(privateKey string) error {
block, _ := pem.Decode([]byte(privateKey))
if block == nil {
return stackerr.New("failed to decode PEM block")
}
priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return stackerr.WithStack(err)
}
err = priv.Validate()
if err != nil {
return stackerr.WithStack(err)
}
return nil
}
@@ -1,809 +0,0 @@
package seedjobs
import (
"context"
"testing"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
var fakePrivateKey = `-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEArK4ld6i2iqW6L3jaTZaKD/v7PjDn+Ik9MXp+kvLcUw/+wEGm
285UwqLnDDlBhSi9nDgJ+m1XU87VCpz/DXW23R/CQcMX2xunib4wWLQqoR3CWbk3
SwiLd8TWAvXkxdXm8fDOGAZbYK2alMV+M+9E2OpZsBUCxmb/3FAofF6JccKoJOH8
UveRNSOx7IXPKtHFiypBhWM4l6ZjgJKm+DRIEhyvoC+pHzcum2ZEPOv+ZJDy5jXK
ZHcNQXVnAZtCcojcjVUBw2rZms+fQ6Volv2JT71Gpykzx/rChhwNwxdAEwjLjKjL
nBWEh/WxsS3NbM7zb4B2XGMCeWVeb/niUwpy+wIDAQABAoIBAQCjGkJNidARmYQI
/u/DxWNWwb2H+o3BFW/1YixYBIjS9BK96cT/bR5mUZRG2XXnnpmqCsxx/AE2KfDU
e4H1ZrB4oFzN3MaVsMNIuZnUzyhM0l0WfnmZp9KEKCm01ilmLCpdcARacPaylIej
6f7QcznmYUShqtbaK8OUhyoWfvz3s0VLkpBlqm63uPtjAx6sAl399THxHVwbYgYy
TxPY8wdjOvNzQJ7ColUh05Zq6TsCGGFUFg7v4to/AXtDhcTMVONlapP+XxekRx8P
98BepIgzgvQhWak8gm+cKQYANk14Q8BDzUCDplYuIZVvKl+/ZHltjHGjrqxDrcDA
0U7REgtxAoGBAN+LAEf2o14ffs/ebVSxiv7LnuAxFh2L6i7RqtehpSf7BnYC65vB
6TMsc/0/KFkD5Az7nrJmA7HmM8J/NI2ks0Mbft+0XCRFx/zfU6pOvPinRKp/8Vtm
aUmNzhz8UMaQ1JXOvBOqvXKWYrN1WPha1+/BnUQrpTdhGxAoAh1FW4eHAoGBAMXA
mXTN5X8+mp9KW2bIpFsjrZ+EyhxO6a6oBMZY54rceeOzf5RcXY7EOiTrnmr+lQvp
fAKBeX5V8G96nSEIDmPhKGZ1C1vEP6hRWahJo1XkN5E1j6hRHCu3DQLtL2lxlyfG
Fx11fysgmLoPVVytLAEQwt4WxMp7OsM1NWqB+u3tAoGBAILUg3Gas7pejIV0FGDB
GCxPV8i2cc8RGBoWs/pHrLVdgUaIJwSd1LISjj/lOuP+FvZSPWsDsZ3osNpgQI21
mwTnjrW2hUblYEprGjhOpOKSYum2v7dSlMRrrfng4hWUphaXTBPmlcH+qf2F7HBO
GptDoZtIQAXNW111TOd8tDj5AoGAC1PO9nvcy38giENQHQEdOQNALMUEdr6mcBS7
wUjSaofai4p6olrwGP9wfTDp8CMJEpebPOGBvhTaIuiZG41ElcAN+mB1+Bmzs8aF
JjihnIfoDu9MfU24GWDw49wGPTn+eI7GQC+8yxGg7fd24kohHSaCowoW16pbYVco
6iLr5rkCgYBt0bcYJ3AOTH0UXS8kvJvnyce/RBIAMoUABwvdkZt9r5B4UzsoLq5e
WrrU6fSRsE6lSsBd83pOAQ46tv+vntQ+0EihD9/0INhkQM99lBw1TFdFTgGSAs1e
ns4JGP6f5uIuwqu/nbqPqMyDovjkGbX2znuGBcvki90Pi97XL7MMWw==
-----END RSA PRIVATE KEY-----`
var fakeInvalidPrivateKey = `-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEArK4ld6i2iqW6L3jaTZaKD/v7PjDn+Ik9MXp+kvLcUw/+wEGm
285UwqLnDDlBhSi9nDgJ+m1XU87VCpz/DXW23R/CQcMX2xunib4wWLQqoR3CWbk3
SwiLd8TWAvXkxdXm8fDOGAZbYK2alMV+M+9E2OpZsBUCxmb/3FAofF6JccKoJOH8
`
func TestValidateSeedJobs(t *testing.T) {
secretTypeMeta := metav1.TypeMeta{
Kind: "Secret",
APIVersion: "v1",
}
secretObjectMeta := metav1.ObjectMeta{
Name: "deploy-keys",
Namespace: "default",
}
t.Run("Valid with public repository and without private key", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
SeedJobs: []v1alpha2.SeedJob{
{
ID: "example",
CredentialID: "jenkins-operator-e2e",
JenkinsCredentialType: v1alpha2.NoJenkinsCredentialCredentialType,
Targets: "cicd/jobs/*.jenkins",
RepositoryBranch: "master",
RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git",
},
},
},
}
fakeClient := fake.NewFakeClient()
config := configuration.Configuration{
Client: fakeClient,
ClientSet: kubernetes.Clientset{},
Notifications: nil,
Jenkins: &v1alpha2.Jenkins{},
}
seedJobs := New(nil, config)
result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err)
assert.Nil(t, result)
})
t.Run("Invalid without id", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
SeedJobs: []v1alpha2.SeedJob{
{
JenkinsCredentialType: v1alpha2.NoJenkinsCredentialCredentialType,
Targets: "cicd/jobs/*.jenkins",
RepositoryBranch: "master",
RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git",
},
},
},
}
fakeClient := fake.NewFakeClient()
config := configuration.Configuration{
Client: fakeClient,
ClientSet: kubernetes.Clientset{},
Notifications: nil,
Jenkins: &v1alpha2.Jenkins{},
}
seedJobs := New(nil, config)
result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err)
assert.Equal(t, result, []string{"seedJob `` id can't be empty"})
})
t.Run("Valid with private key and secret", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
SeedJobs: []v1alpha2.SeedJob{
{
ID: "example",
CredentialID: "deploy-keys",
JenkinsCredentialType: v1alpha2.BasicSSHCredentialType,
Targets: "cicd/jobs/*.jenkins",
RepositoryBranch: "master",
RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git",
},
},
},
}
secret := &corev1.Secret{
TypeMeta: secretTypeMeta,
ObjectMeta: secretObjectMeta,
Data: map[string][]byte{
UsernameSecretKey: []byte("username"),
PrivateKeySecretKey: []byte(fakePrivateKey),
},
}
fakeClient := fake.NewFakeClient()
err := fakeClient.Create(context.TODO(), secret)
assert.NoError(t, err)
config := configuration.Configuration{
Client: fakeClient,
ClientSet: kubernetes.Clientset{},
Notifications: nil,
Jenkins: &v1alpha2.Jenkins{},
}
seedJobs := New(nil, config)
result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err)
assert.Nil(t, result)
})
t.Run("Invalid private key in secret", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
SeedJobs: []v1alpha2.SeedJob{
{
ID: "example",
CredentialID: "deploy-keys",
JenkinsCredentialType: v1alpha2.BasicSSHCredentialType,
Targets: "cicd/jobs/*.jenkins",
RepositoryBranch: "master",
RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git",
},
},
},
}
secret := &corev1.Secret{
TypeMeta: secretTypeMeta,
ObjectMeta: secretObjectMeta,
Data: map[string][]byte{
UsernameSecretKey: []byte("username"),
PrivateKeySecretKey: []byte(fakeInvalidPrivateKey),
},
}
fakeClient := fake.NewFakeClient()
err := fakeClient.Create(context.TODO(), secret)
assert.NoError(t, err)
config := configuration.Configuration{
Client: fakeClient,
ClientSet: kubernetes.Clientset{},
Notifications: nil,
Jenkins: &v1alpha2.Jenkins{},
}
seedJobs := New(nil, config)
result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err)
assert.Equal(t, result, []string{"seedJob `example` private key 'privateKey' invalid in secret 'deploy-keys': failed to decode PEM block"})
})
t.Run("Invalid with PrivateKey and empty Secret data", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
SeedJobs: []v1alpha2.SeedJob{
{
ID: "example",
CredentialID: "deploy-keys",
JenkinsCredentialType: v1alpha2.BasicSSHCredentialType,
Targets: "cicd/jobs/*.jenkins",
RepositoryBranch: "master",
RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git",
},
},
},
}
secret := &corev1.Secret{
TypeMeta: secretTypeMeta,
ObjectMeta: secretObjectMeta,
Data: map[string][]byte{
UsernameSecretKey: []byte("username"),
PrivateKeySecretKey: []byte(""),
},
}
fakeClient := fake.NewFakeClient()
err := fakeClient.Create(context.TODO(), secret)
assert.NoError(t, err)
config := configuration.Configuration{
Client: fakeClient,
ClientSet: kubernetes.Clientset{},
Notifications: nil,
Jenkins: &v1alpha2.Jenkins{},
}
seedJobs := New(nil, config)
result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err)
assert.Equal(t, result, []string{"seedJob `example` required data 'privateKey' not found in secret 'deploy-keys'", "seedJob `example` private key 'privateKey' invalid in secret 'deploy-keys': failed to decode PEM block"})
})
t.Run("Invalid with ssh RepositoryURL and empty PrivateKey", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
SeedJobs: []v1alpha2.SeedJob{
{
ID: "example",
CredentialID: "jenkins-operator-e2e",
JenkinsCredentialType: v1alpha2.BasicSSHCredentialType,
Targets: "cicd/jobs/*.jenkins",
RepositoryBranch: "master",
RepositoryURL: "git@github.com:jenkinsci/kubernetes-operator.git",
},
},
},
}
fakeClient := fake.NewFakeClient()
config := configuration.Configuration{
Client: fakeClient,
ClientSet: kubernetes.Clientset{},
Notifications: nil,
Jenkins: &v1alpha2.Jenkins{},
}
seedJobs := New(nil, config)
result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err)
assert.Equal(t, result, []string{"seedJob `example` required secret 'jenkins-operator-e2e' with Jenkins credential not found", "seedJob `example` required data 'username' not found in secret ''", "seedJob `example` required data 'username' is empty in secret ''", "seedJob `example` required data 'privateKey' not found in secret ''", "seedJob `example` required data 'privateKey' not found in secret ''", "seedJob `example` private key 'privateKey' invalid in secret '': failed to decode PEM block"})
})
t.Run("Invalid without targets", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
SeedJobs: []v1alpha2.SeedJob{
{
ID: "example",
JenkinsCredentialType: v1alpha2.NoJenkinsCredentialCredentialType,
RepositoryBranch: "master",
RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git",
},
},
},
}
fakeClient := fake.NewFakeClient()
config := configuration.Configuration{
Client: fakeClient,
ClientSet: kubernetes.Clientset{},
Notifications: nil,
Jenkins: &v1alpha2.Jenkins{},
}
seedJobs := New(nil, config)
result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err)
assert.Equal(t, result, []string{"seedJob `example` targets can't be empty"})
})
t.Run("Invalid without repository URL", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
SeedJobs: []v1alpha2.SeedJob{
{
ID: "example",
JenkinsCredentialType: v1alpha2.NoJenkinsCredentialCredentialType,
Targets: "cicd/jobs/*.jenkins",
RepositoryBranch: "master",
},
},
},
}
fakeClient := fake.NewFakeClient()
config := configuration.Configuration{
Client: fakeClient,
ClientSet: kubernetes.Clientset{},
Notifications: nil,
Jenkins: &v1alpha2.Jenkins{},
}
seedJobs := New(nil, config)
result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err)
assert.Equal(t, result, []string{"seedJob `example` repository URL branch can't be empty"})
})
t.Run("Invalid without repository branch", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
SeedJobs: []v1alpha2.SeedJob{
{
ID: "example",
JenkinsCredentialType: v1alpha2.NoJenkinsCredentialCredentialType,
Targets: "cicd/jobs/*.jenkins",
RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git",
},
},
},
}
fakeClient := fake.NewFakeClient()
config := configuration.Configuration{
Client: fakeClient,
ClientSet: kubernetes.Clientset{},
Notifications: nil,
Jenkins: &v1alpha2.Jenkins{},
}
seedJobs := New(nil, config)
result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err)
assert.Equal(t, result, []string{"seedJob `example` repository branch can't be empty"})
})
t.Run("Valid with username and password", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
SeedJobs: []v1alpha2.SeedJob{
{
ID: "example",
CredentialID: "deploy-keys",
JenkinsCredentialType: v1alpha2.UsernamePasswordCredentialType,
Targets: "cicd/jobs/*.jenkins",
RepositoryBranch: "master",
RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git",
},
},
},
}
secret := &corev1.Secret{
TypeMeta: secretTypeMeta,
ObjectMeta: secretObjectMeta,
Data: map[string][]byte{
UsernameSecretKey: []byte("some-username"),
PasswordSecretKey: []byte("some-password"),
},
}
fakeClient := fake.NewFakeClient()
err := fakeClient.Create(context.TODO(), secret)
assert.NoError(t, err)
config := configuration.Configuration{
Client: fakeClient,
ClientSet: kubernetes.Clientset{},
Notifications: nil,
Jenkins: &v1alpha2.Jenkins{},
}
seedJobs := New(nil, config)
result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err)
assert.Nil(t, result)
})
t.Run("Invalid with empty username", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
SeedJobs: []v1alpha2.SeedJob{
{
ID: "example",
CredentialID: "deploy-keys",
JenkinsCredentialType: v1alpha2.UsernamePasswordCredentialType,
Targets: "cicd/jobs/*.jenkins",
RepositoryBranch: "master",
RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git",
},
},
},
}
secret := &corev1.Secret{
TypeMeta: secretTypeMeta,
ObjectMeta: secretObjectMeta,
Data: map[string][]byte{
UsernameSecretKey: []byte(""),
PasswordSecretKey: []byte("some-password"),
},
}
fakeClient := fake.NewFakeClient()
err := fakeClient.Create(context.TODO(), secret)
assert.NoError(t, err)
config := configuration.Configuration{
Client: fakeClient,
ClientSet: kubernetes.Clientset{},
Notifications: nil,
Jenkins: &v1alpha2.Jenkins{},
}
seedJobs := New(nil, config)
result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err)
assert.Equal(t, result, []string{"seedJob `example` required data 'username' is empty in secret 'deploy-keys'"})
})
t.Run("Invalid with empty password", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
SeedJobs: []v1alpha2.SeedJob{
{
ID: "example",
CredentialID: "deploy-keys",
JenkinsCredentialType: v1alpha2.UsernamePasswordCredentialType,
Targets: "cicd/jobs/*.jenkins",
RepositoryBranch: "master",
RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git",
},
},
},
}
secret := &corev1.Secret{
TypeMeta: secretTypeMeta,
ObjectMeta: secretObjectMeta,
Data: map[string][]byte{
UsernameSecretKey: []byte("some-username"),
PasswordSecretKey: []byte(""),
},
}
fakeClient := fake.NewFakeClient()
err := fakeClient.Create(context.TODO(), secret)
assert.NoError(t, err)
config := configuration.Configuration{
Client: fakeClient,
ClientSet: kubernetes.Clientset{},
Notifications: nil,
Jenkins: &v1alpha2.Jenkins{},
}
seedJobs := New(nil, config)
result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err)
assert.Equal(t, result, []string{"seedJob `example` required data 'password' is empty in secret 'deploy-keys'"})
})
t.Run("Invalid without username", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
SeedJobs: []v1alpha2.SeedJob{
{
ID: "example",
CredentialID: "deploy-keys",
JenkinsCredentialType: v1alpha2.UsernamePasswordCredentialType,
Targets: "cicd/jobs/*.jenkins",
RepositoryBranch: "master",
RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git",
},
},
},
}
secret := &corev1.Secret{
TypeMeta: secretTypeMeta,
ObjectMeta: secretObjectMeta,
Data: map[string][]byte{
PasswordSecretKey: []byte("some-password"),
},
}
fakeClient := fake.NewFakeClient()
err := fakeClient.Create(context.TODO(), secret)
assert.NoError(t, err)
config := configuration.Configuration{
Client: fakeClient,
ClientSet: kubernetes.Clientset{},
Notifications: nil,
Jenkins: &v1alpha2.Jenkins{},
}
seedJobs := New(nil, config)
result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err)
assert.Equal(t, result, []string{"seedJob `example` required data 'username' not found in secret 'deploy-keys'", "seedJob `example` required data 'username' is empty in secret 'deploy-keys'"})
})
t.Run("Invalid without password", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
SeedJobs: []v1alpha2.SeedJob{
{
ID: "example",
CredentialID: "deploy-keys",
JenkinsCredentialType: v1alpha2.UsernamePasswordCredentialType,
Targets: "cicd/jobs/*.jenkins",
RepositoryBranch: "master",
RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git",
},
},
},
}
secret := &corev1.Secret{
TypeMeta: secretTypeMeta,
ObjectMeta: secretObjectMeta,
Data: map[string][]byte{
UsernameSecretKey: []byte("some-username"),
},
}
fakeClient := fake.NewFakeClient()
err := fakeClient.Create(context.TODO(), secret)
assert.NoError(t, err)
config := configuration.Configuration{
Client: fakeClient,
ClientSet: kubernetes.Clientset{},
Notifications: nil,
Jenkins: &v1alpha2.Jenkins{},
}
seedJobs := New(nil, config)
result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err)
assert.Equal(t, result, []string{"seedJob `example` required data 'password' not found in secret 'deploy-keys'", "seedJob `example` required data 'password' is empty in secret 'deploy-keys'"})
})
t.Run("Invalid with wrong cron spec", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
SeedJobs: []v1alpha2.SeedJob{
{
ID: "example",
CredentialID: "jenkins-operator-e2e",
JenkinsCredentialType: v1alpha2.NoJenkinsCredentialCredentialType,
Targets: "cicd/jobs/*.jenkins",
RepositoryBranch: "master",
RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git",
BuildPeriodically: "invalid-cron-spec",
},
},
},
}
fakeClient := fake.NewFakeClient()
config := configuration.Configuration{
Client: fakeClient,
ClientSet: kubernetes.Clientset{},
Notifications: nil,
Jenkins: &v1alpha2.Jenkins{},
}
seedJobs := New(nil, config)
result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err)
assert.Equal(t, result, []string{"seedJob `example` `buildPeriodically` schedule 'invalid-cron-spec' is invalid cron spec in `example`"})
})
t.Run("Valid with good cron spec", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
SeedJobs: []v1alpha2.SeedJob{
{
ID: "example",
CredentialID: "jenkins-operator-e2e",
JenkinsCredentialType: v1alpha2.NoJenkinsCredentialCredentialType,
Targets: "cicd/jobs/*.jenkins",
RepositoryBranch: "master",
RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git",
BuildPeriodically: "1 2 3 4 5",
PollSCM: "1 2 3 4 5",
},
},
},
}
fakeClient := fake.NewFakeClient()
config := configuration.Configuration{
Client: fakeClient,
ClientSet: kubernetes.Clientset{},
Notifications: nil,
Jenkins: &v1alpha2.Jenkins{},
}
seedJobs := New(nil, config)
result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err)
assert.Nil(t, result)
})
t.Run("Invalid with set githubPushTrigger and not installed github plugin", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
SeedJobs: []v1alpha2.SeedJob{
{
ID: "example",
CredentialID: "jenkins-operator-e2e",
JenkinsCredentialType: v1alpha2.NoJenkinsCredentialCredentialType,
Targets: "cicd/jobs/*.jenkins",
RepositoryBranch: "master",
RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git",
GitHubPushTrigger: true,
},
},
},
}
fakeClient := fake.NewFakeClient()
config := configuration.Configuration{
Client: fakeClient,
ClientSet: kubernetes.Clientset{},
Notifications: nil,
Jenkins: &v1alpha2.Jenkins{},
}
seedJobs := New(nil, config)
result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err)
assert.Equal(t, result, []string{"seedJob `example` githubPushTrigger cannot be enabled: `github` plugin not installed"})
})
t.Run("Valid with set githubPushTrigger and installed github plugin", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
SeedJobs: []v1alpha2.SeedJob{
{
ID: "example",
CredentialID: "jenkins-operator-e2e",
JenkinsCredentialType: v1alpha2.NoJenkinsCredentialCredentialType,
Targets: "cicd/jobs/*.jenkins",
RepositoryBranch: "master",
RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git",
GitHubPushTrigger: true,
},
},
Master: v1alpha2.JenkinsMaster{
Plugins: []v1alpha2.Plugin{
{Name: "github", Version: "latest"},
},
},
},
}
fakeClient := fake.NewFakeClient()
config := configuration.Configuration{
Client: fakeClient,
ClientSet: kubernetes.Clientset{},
Notifications: nil,
Jenkins: &v1alpha2.Jenkins{},
}
seedJobs := New(nil, config)
result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err)
assert.Nil(t, result)
})
t.Run("Invalid with set bitbucketPushTrigger and not installed bitbucket plugin", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
SeedJobs: []v1alpha2.SeedJob{
{
ID: "example",
CredentialID: "jenkins-operator-e2e",
JenkinsCredentialType: v1alpha2.NoJenkinsCredentialCredentialType,
Targets: "cicd/jobs/*.jenkins",
RepositoryBranch: "master",
RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git",
BitbucketPushTrigger: true,
},
},
},
}
fakeClient := fake.NewFakeClient()
config := configuration.Configuration{
Client: fakeClient,
ClientSet: kubernetes.Clientset{},
Notifications: nil,
Jenkins: &v1alpha2.Jenkins{},
}
seedJobs := New(nil, config)
result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err)
assert.Equal(t, result, []string{"seedJob `example` bitbucketPushTrigger cannot be enabled: `bitbucket` plugin not installed"})
})
t.Run("Valid with set bitbucketPushTrigger and installed Bitbucket plugin", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{
Spec: v1alpha2.JenkinsSpec{
SeedJobs: []v1alpha2.SeedJob{
{
ID: "example",
CredentialID: "jenkins-operator-e2e",
JenkinsCredentialType: v1alpha2.NoJenkinsCredentialCredentialType,
Targets: "cicd/jobs/*.jenkins",
RepositoryBranch: "master",
RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git",
BitbucketPushTrigger: true,
},
},
Master: v1alpha2.JenkinsMaster{
Plugins: []v1alpha2.Plugin{
{Name: "bitbucket", Version: "latest"},
},
},
},
}
fakeClient := fake.NewFakeClient()
config := configuration.Configuration{
Client: fakeClient,
ClientSet: kubernetes.Clientset{},
Notifications: nil,
Jenkins: &v1alpha2.Jenkins{},
}
seedJobs := New(nil, config)
result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err)
assert.Nil(t, result)
})
}
func TestValidateIfIDIsUnique(t *testing.T) {
t.Run("happy", func(t *testing.T) {
seedJobs := []v1alpha2.SeedJob{
{ID: "first"}, {ID: "second"},
}
config := configuration.Configuration{
Client: nil,
ClientSet: kubernetes.Clientset{},
Notifications: nil,
Jenkins: &v1alpha2.Jenkins{},
}
ctrl := New(nil, config)
got := ctrl.validateIfIDIsUnique(seedJobs)
assert.Nil(t, got)
})
t.Run("duplicated ids", func(t *testing.T) {
seedJobs := []v1alpha2.SeedJob{
{ID: "first"}, {ID: "first"},
}
config := configuration.Configuration{
Client: nil,
ClientSet: kubernetes.Clientset{},
Notifications: nil,
Jenkins: &v1alpha2.Jenkins{},
}
ctrl := New(nil, config)
got := ctrl.validateIfIDIsUnique(seedJobs)
assert.Equal(t, got, []string{"'first' seed job ID is not unique"})
})
}
@@ -1,18 +0,0 @@
package user
import (
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/backuprestore"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/user/seedjobs"
)
// Validate validates Jenkins CR Spec section
func (r *reconcileUserConfiguration) Validate(jenkins *v1alpha2.Jenkins) ([]string, error) {
backupAndRestore := backuprestore.New(r.Configuration, r.logger)
if msg := backupAndRestore.Validate(); msg != nil {
return msg, nil
}
seedJobs := seedjobs.New(r.jenkinsClient, r.Configuration)
return seedJobs.ValidateSeedJobs(*jenkins)
}
@@ -1,18 +0,0 @@
package constants
const (
// OperatorName is a operator name
OperatorName = "jenkins-operator"
// DefaultAmountOfExecutors is the default amount of Jenkins executors
DefaultAmountOfExecutors = 0
// SeedJobSuffix is a suffix added for all seed jobs
SeedJobSuffix = "job-dsl-seed"
// DefaultJenkinsMasterImage is the default Jenkins master docker image
DefaultJenkinsMasterImage = "jenkins/jenkins:lts"
// DefaultHTTPPortInt32 is the default Jenkins HTTP port
DefaultHTTPPortInt32 = int32(8080)
// DefaultSlavePortInt32 is the default Jenkins port for slaves
DefaultSlavePortInt32 = int32(50000)
// JavaOpsVariableName is the name of environment variable which consists Jenkins Java options
JavaOpsVariableName = "JAVA_OPTS"
)
@@ -1,16 +0,0 @@
package constants
const (
// LabelAppKey application Kubernetes label name
LabelAppKey = "app"
// LabelAppValue application Kubernetes label value
LabelAppValue = OperatorName
// LabelWatchKey Kubernetes label used to enable watch for reconcile loop
LabelWatchKey = "watch"
// LabelWatchValue Kubernetes label value to enable watch for reconcile loop
LabelWatchValue = "true"
// LabelJenkinsCRKey Kubernetes label name which contains Jenkins CR name
LabelJenkinsCRKey = "jenkins-cr"
)
-2
View File
@@ -1,2 +0,0 @@
// Package groovy implements groovy scripts execution via Jenkins Job
package groovy
-251
View File
@@ -1,251 +0,0 @@
package groovy
import (
"context"
"crypto/sha256"
"encoding/base64"
"fmt"
"sort"
"strings"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client"
"github.com/jenkinsci/kubernetes-operator/pkg/log"
"github.com/go-logr/logr"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
k8s "sigs.k8s.io/controller-runtime/pkg/client"
)
// Groovy defines API for groovy secrets execution via jenkins job
type Groovy struct {
k8sClient k8s.Client
logger logr.Logger
jenkins *v1alpha2.Jenkins
jenkinsClient jenkinsclient.Jenkins
configurationType string
customization v1alpha2.Customization
}
// New creates new instance of Groovy
func New(jenkinsClient jenkinsclient.Jenkins, k8sClient k8s.Client, jenkins *v1alpha2.Jenkins, configurationType string, customization v1alpha2.Customization) *Groovy {
return &Groovy{
jenkinsClient: jenkinsClient,
k8sClient: k8sClient,
jenkins: jenkins,
configurationType: configurationType,
customization: customization,
logger: log.Log.WithValues("cr", jenkins.Name),
}
}
// EnsureSingle runs single groovy script
func (g *Groovy) EnsureSingle(source, name, hash, groovyScript string) (requeue bool, err error) {
if g.isGroovyScriptAlreadyApplied(source, name, hash) {
return false, nil
}
logs, err := g.jenkinsClient.ExecuteScript(groovyScript)
if err != nil {
if groovyErr, ok := err.(*jenkinsclient.GroovyScriptExecutionFailed); ok {
groovyErr.ConfigurationType = g.configurationType
groovyErr.Name = name
groovyErr.Source = source
groovyErr.Logs = logs
g.logger.V(log.VWarn).Info(fmt.Sprintf("%s Source '%s' Name '%s' groovy script execution failed, logs :\n%s", g.configurationType, source, name, logs))
}
return true, err
}
var appliedGroovyScripts []v1alpha2.AppliedGroovyScript
for _, ags := range g.jenkins.Status.AppliedGroovyScripts {
if g.configurationType == ags.ConfigurationType && ags.Source == source && ags.Name == name {
continue
}
appliedGroovyScripts = append(appliedGroovyScripts, ags)
}
appliedGroovyScripts = append(appliedGroovyScripts, v1alpha2.AppliedGroovyScript{
ConfigurationType: g.configurationType,
Source: source,
Name: name,
Hash: hash,
})
g.jenkins.Status.AppliedGroovyScripts = appliedGroovyScripts
return true, g.k8sClient.Update(context.TODO(), g.jenkins)
}
// WaitForSecretSynchronization runs groovy script which waits to synchronize secrets in pod by k8s
func (g *Groovy) WaitForSecretSynchronization(secretsPath string) (requeue bool, err error) {
if len(g.customization.Secret.Name) == 0 {
return false, nil
}
secret := &corev1.Secret{}
err = g.k8sClient.Get(context.TODO(), types.NamespacedName{Name: g.customization.Secret.Name, Namespace: g.jenkins.ObjectMeta.Namespace}, secret)
if err != nil {
return true, errors.WithStack(err)
}
toCalculate := map[string]string{}
for secretKey, secretValue := range secret.Data {
toCalculate[secretKey] = string(secretValue)
}
hash := g.calculateHash(toCalculate)
name := "synchronizing-secret.groovy"
if g.isGroovyScriptAlreadyApplied(g.customization.Secret.Name, name, hash) {
return false, nil
}
g.logger.Info(fmt.Sprintf("%s Secret '%s' running synchronization", g.configurationType, secret.Name))
return g.EnsureSingle(g.customization.Secret.Name, name, hash, fmt.Sprintf(synchronizeSecretsGroovyScriptFmt, secretsPath, hash))
}
// Ensure runs all groovy scripts configured in customization structure
func (g *Groovy) Ensure(filter func(name string) bool, updateGroovyScript func(groovyScript string) string) (requeue bool, err error) {
secret := &corev1.Secret{}
if len(g.customization.Secret.Name) > 0 {
err := g.k8sClient.Get(context.TODO(), types.NamespacedName{Name: g.customization.Secret.Name, Namespace: g.jenkins.ObjectMeta.Namespace}, secret)
if err != nil {
return true, err
}
}
for _, configMapRef := range g.customization.Configurations {
configMap := &corev1.ConfigMap{}
err := g.k8sClient.Get(context.TODO(), types.NamespacedName{Name: configMapRef.Name, Namespace: g.jenkins.ObjectMeta.Namespace}, configMap)
if err != nil {
return true, errors.WithStack(err)
}
var names []string
for name := range configMap.Data {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
groovyScript := updateGroovyScript(configMap.Data[name])
if !filter(name) {
g.logger.V(log.VDebug).Info(fmt.Sprintf("Skipping %s ConfigMap '%s' name '%s'", g.configurationType, configMap.Name, name))
continue
}
hash := g.calculateCustomizationHash(*secret, name, groovyScript)
if g.isGroovyScriptAlreadyApplied(configMap.Name, name, hash) {
continue
}
g.logger.Info(fmt.Sprintf("%s ConfigMap '%s' name '%s' running groovy script", g.configurationType, configMap.Name, name))
requeue, err := g.EnsureSingle(configMap.Name, name, hash, groovyScript)
if err != nil || requeue {
return requeue, err
}
}
}
return false, nil
}
func (g *Groovy) calculateCustomizationHash(secret corev1.Secret, key, groovyScript string) string {
toCalculate := map[string]string{}
for secretKey, secretValue := range secret.Data {
toCalculate[secretKey] = string(secretValue)
}
toCalculate[key] = groovyScript
return g.calculateHash(toCalculate)
}
func (g *Groovy) isGroovyScriptAlreadyApplied(source, name, hash string) bool {
for _, appliedGroovyScript := range g.jenkins.Status.AppliedGroovyScripts {
if appliedGroovyScript.ConfigurationType == g.configurationType && appliedGroovyScript.Hash == hash &&
appliedGroovyScript.Name == name && appliedGroovyScript.Source == source {
return true
}
}
return false
}
func (g *Groovy) calculateHash(data map[string]string) string {
hash := sha256.New()
var keys []string
for key := range data {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
hash.Write([]byte(key))
hash.Write([]byte(data[key]))
}
return base64.StdEncoding.EncodeToString(hash.Sum(nil))
}
// AddSecretsLoaderToGroovyScript modify groovy scripts to load Kubernetes secrets into groovy map
func AddSecretsLoaderToGroovyScript(secretsPath string) func(groovyScript string) string {
return func(groovyScript string) string {
if !strings.HasPrefix(groovyScript, importPrefix) {
return fmt.Sprintf(secretsLoaderGroovyScriptFmt, secretsPath) + groovyScript
}
lines := strings.Split(groovyScript, "\n")
importIndex := -1
for i, line := range lines {
if !strings.HasPrefix(line, importPrefix) {
importIndex = i
break
}
}
return strings.Join(lines[:importIndex], "\n") + "\n\n" + fmt.Sprintf(secretsLoaderGroovyScriptFmt, secretsPath) + "\n\n" + strings.Join(lines[importIndex:], "\n")
}
}
const importPrefix = "import "
const secretsLoaderGroovyScriptFmt = `def secretsPath = '%s'
def secrets = [:]
"ls ${secretsPath}".execute().text.eachLine {secrets[it] = new File("${secretsPath}/${it}").text}`
const synchronizeSecretsGroovyScriptFmt = `
def secretsPath = '%s'
def expectedHash = '%s'
println "Synchronizing Kubernetes Secret to the Jenkins master pod, timeout 60 seconds."
def complete = false
for(int i = 1; i <= 30; i++) {
def fileList = "ls ${secretsPath}".execute()
def secrets = []
fileList .text.eachLine {secrets.add(it)}
println "Mounted secrets: ${secrets}"
def actualHash = calculateHash((String[])secrets, secretsPath)
println "Expected hash '${expectedHash}', actual hash '${actualHash}', will retry"
if(expectedHash == actualHash) {
complete = true
break
}
sleep 2000
}
if(!complete) {
throw new Exception("Timeout while synchronizing files")
}
def calculateHash(String[] secrets, String secretsPath) {
def hash = java.security.MessageDigest.getInstance("SHA-256")
for(secret in secrets) {
hash.update(secret.getBytes())
def fileLocation = java.nio.file.Paths.get("${secretsPath}/${secret}")
def fileData = java.nio.file.Files.readAllBytes(fileLocation)
hash.update(fileData)
}
return Base64.getEncoder().encodeToString(hash.digest())
}
`
@@ -1,705 +0,0 @@
package groovy
import (
"context"
"fmt"
"strings"
"testing"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client"
"github.com/golang/mock/gomock"
"github.com/jenkinsci/kubernetes-operator/pkg/log"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
const configurationType = "test-conf-type"
func TestGroovy_EnsureSingle(t *testing.T) {
log.SetupLogger(true)
emptyCustomization := v1alpha2.Customization{}
hash := "hash"
groovyScript := "groovy-script"
groovyScriptName := "groovy-script-name"
source := "source"
ctx := context.TODO()
jenkinsName := "jenkins"
namespace := "default"
t.Run("execute script and save status", func(t *testing.T) {
// given
jenkins := &v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Name: jenkinsName,
Namespace: namespace,
},
}
err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme)
require.NoError(t, err)
fakeClient := fake.NewFakeClient()
err = fakeClient.Create(ctx, jenkins)
require.NoError(t, err)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
jenkinsClient := jenkinsclient.NewMockJenkins(ctrl)
jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil)
groovyClient := New(jenkinsClient, fakeClient, jenkins, configurationType, emptyCustomization)
// when
requeue, err := groovyClient.EnsureSingle(source, groovyScriptName, hash, groovyScript)
// then
require.NoError(t, err)
assert.True(t, requeue)
err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins)
require.NoError(t, err)
assert.Equal(t, 1, len(jenkins.Status.AppliedGroovyScripts))
assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[0].ConfigurationType)
assert.Equal(t, hash, jenkins.Status.AppliedGroovyScripts[0].Hash)
assert.Equal(t, source, jenkins.Status.AppliedGroovyScripts[0].Source)
assert.Equal(t, groovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name)
})
t.Run("no execute script", func(t *testing.T) {
// given
jenkins := &v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Name: jenkinsName,
Namespace: namespace,
},
Status: v1alpha2.JenkinsStatus{
AppliedGroovyScripts: []v1alpha2.AppliedGroovyScript{
{
ConfigurationType: configurationType,
Source: source,
Name: groovyScriptName,
Hash: hash,
},
},
},
}
err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme)
require.NoError(t, err)
fakeClient := fake.NewFakeClient()
err = fakeClient.Create(ctx, jenkins)
require.NoError(t, err)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
jenkinsClient := jenkinsclient.NewMockJenkins(ctrl)
groovyClient := New(jenkinsClient, fakeClient, jenkins, configurationType, emptyCustomization)
// when
requeue, err := groovyClient.EnsureSingle(source, groovyScriptName, hash, groovyScript)
// then
require.NoError(t, err)
assert.False(t, requeue)
err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins)
require.NoError(t, err)
assert.Equal(t, 1, len(jenkins.Status.AppliedGroovyScripts))
assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[0].ConfigurationType)
assert.Equal(t, hash, jenkins.Status.AppliedGroovyScripts[0].Hash)
assert.Equal(t, source, jenkins.Status.AppliedGroovyScripts[0].Source)
assert.Equal(t, groovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name)
})
t.Run("execute script with new version", func(t *testing.T) {
anotherHash := "hash1"
// given
jenkins := &v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Name: jenkinsName,
Namespace: namespace,
},
}
err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme)
require.NoError(t, err)
fakeClient := fake.NewFakeClient()
err = fakeClient.Create(ctx, jenkins)
require.NoError(t, err)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
jenkinsClient := jenkinsclient.NewMockJenkins(ctrl)
jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil)
jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil)
jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil)
groovyClient := New(jenkinsClient, fakeClient, jenkins, configurationType, emptyCustomization)
// when
requeue, err := groovyClient.EnsureSingle(source, groovyScriptName, hash, groovyScript)
// then
require.NoError(t, err)
assert.True(t, requeue)
err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins)
require.NoError(t, err)
assert.Equal(t, 1, len(jenkins.Status.AppliedGroovyScripts))
assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[0].ConfigurationType)
assert.Equal(t, hash, jenkins.Status.AppliedGroovyScripts[0].Hash)
assert.Equal(t, source, jenkins.Status.AppliedGroovyScripts[0].Source)
assert.Equal(t, groovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name)
// Update with new hash
requeue, err = groovyClient.EnsureSingle(source, groovyScriptName, anotherHash, groovyScript)
require.NoError(t, err)
assert.True(t, requeue)
err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins)
require.NoError(t, err)
assert.Equal(t, 1, len(jenkins.Status.AppliedGroovyScripts))
assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[0].ConfigurationType)
assert.Equal(t, anotherHash, jenkins.Status.AppliedGroovyScripts[0].Hash)
assert.Equal(t, source, jenkins.Status.AppliedGroovyScripts[0].Source)
assert.Equal(t, groovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name)
requeue, err = groovyClient.EnsureSingle(source, groovyScriptName, hash, groovyScript)
require.NoError(t, err)
assert.True(t, requeue)
err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins)
require.NoError(t, err)
assert.Equal(t, 1, len(jenkins.Status.AppliedGroovyScripts))
assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[0].ConfigurationType)
assert.Equal(t, hash, jenkins.Status.AppliedGroovyScripts[0].Hash)
assert.Equal(t, source, jenkins.Status.AppliedGroovyScripts[0].Source)
assert.Equal(t, groovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name)
})
t.Run("execute three groovy scripts with another hash", func(t *testing.T) {
// given
firstGroovyScriptName := "testGroovy1"
firstGroovyScriptHash := "testHash1"
secondGroovyScriptName := "testGroovy2"
secondGroovyScriptHash := "testHash2"
thirdGroovyScriptName := "testGroovy3"
thirdGroovyScriptHash := "testHash3"
jenkins := &v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Name: jenkinsName,
Namespace: namespace,
},
}
err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme)
require.NoError(t, err)
fakeClient := fake.NewFakeClient()
err = fakeClient.Create(ctx, jenkins)
require.NoError(t, err)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
jenkinsClient := jenkinsclient.NewMockJenkins(ctrl)
jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil)
jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil)
jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil)
groovyClient := New(jenkinsClient, fakeClient, jenkins, configurationType, emptyCustomization)
requeue, err := groovyClient.EnsureSingle(source, firstGroovyScriptName, firstGroovyScriptHash, groovyScript)
// then
require.NoError(t, err)
assert.True(t, requeue)
err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins)
require.NoError(t, err)
assert.Equal(t, 1, len(jenkins.Status.AppliedGroovyScripts))
assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[0].ConfigurationType)
assert.Equal(t, firstGroovyScriptHash, jenkins.Status.AppliedGroovyScripts[0].Hash)
assert.Equal(t, source, jenkins.Status.AppliedGroovyScripts[0].Source)
assert.Equal(t, firstGroovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name)
requeue, err = groovyClient.EnsureSingle(source, secondGroovyScriptName, secondGroovyScriptHash, groovyScript)
// then
require.NoError(t, err)
assert.True(t, requeue)
err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins)
require.NoError(t, err)
assert.Equal(t, 2, len(jenkins.Status.AppliedGroovyScripts))
assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[1].ConfigurationType)
assert.Equal(t, secondGroovyScriptHash, jenkins.Status.AppliedGroovyScripts[1].Hash)
assert.Equal(t, source, jenkins.Status.AppliedGroovyScripts[1].Source)
assert.Equal(t, secondGroovyScriptName, jenkins.Status.AppliedGroovyScripts[1].Name)
requeue, err = groovyClient.EnsureSingle(source, thirdGroovyScriptName, thirdGroovyScriptHash, groovyScript)
// then
require.NoError(t, err)
assert.True(t, requeue)
err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins)
require.NoError(t, err)
assert.Equal(t, 3, len(jenkins.Status.AppliedGroovyScripts))
assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[2].ConfigurationType)
assert.Equal(t, thirdGroovyScriptHash, jenkins.Status.AppliedGroovyScripts[2].Hash)
assert.Equal(t, source, jenkins.Status.AppliedGroovyScripts[2].Source)
assert.Equal(t, thirdGroovyScriptName, jenkins.Status.AppliedGroovyScripts[2].Name)
})
t.Run("execute two groovy scripts with same names in two config maps", func(t *testing.T) {
jenkins := &v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Name: jenkinsName,
Namespace: namespace,
},
}
err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme)
require.NoError(t, err)
fakeClient := fake.NewFakeClient()
err = fakeClient.Create(ctx, jenkins)
require.NoError(t, err)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
jenkinsClient := jenkinsclient.NewMockJenkins(ctrl)
jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil)
jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil)
groovyClient := New(jenkinsClient, fakeClient, jenkins, configurationType, emptyCustomization)
requeue, err := groovyClient.EnsureSingle("test-conf1", "test.groovy", hash, groovyScript)
require.NoError(t, err)
assert.True(t, requeue)
requeue, err = groovyClient.EnsureSingle("test-conf2", "test.groovy", "anotherHash", groovyScript)
require.NoError(t, err)
assert.True(t, requeue)
assert.Equal(t, 2, len(jenkins.Status.AppliedGroovyScripts))
})
t.Run("execute two groovy scripts with different configuration types", func(t *testing.T) {
jenkins := &v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Name: jenkinsName,
Namespace: namespace,
},
}
err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme)
require.NoError(t, err)
fakeClient := fake.NewFakeClient()
err = fakeClient.Create(ctx, jenkins)
require.NoError(t, err)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
jenkinsClient := jenkinsclient.NewMockJenkins(ctrl)
jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil)
jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil)
groovyClient := New(jenkinsClient, fakeClient, jenkins, configurationType, emptyCustomization)
requeue, err := groovyClient.EnsureSingle(source, "test.groovy", hash, groovyScript)
require.NoError(t, err)
assert.True(t, requeue)
groovyClient = New(jenkinsClient, fakeClient, jenkins, "another-test-configuration-type", emptyCustomization)
requeue, err = groovyClient.EnsureSingle(source, "test.groovy", "anotherHash", groovyScript)
require.NoError(t, err)
assert.True(t, requeue)
assert.Equal(t, 2, len(jenkins.Status.AppliedGroovyScripts))
})
t.Run("execute script fails", func(t *testing.T) {
// given
jenkins := &v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Name: jenkinsName,
Namespace: namespace,
},
}
err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme)
require.NoError(t, err)
fakeClient := fake.NewFakeClient()
err = fakeClient.Create(ctx, jenkins)
require.NoError(t, err)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
jenkinsClient := jenkinsclient.NewMockJenkins(ctrl)
jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("fail logs", &jenkinsclient.GroovyScriptExecutionFailed{})
groovyClient := New(jenkinsClient, fakeClient, jenkins, configurationType, emptyCustomization)
// when
requeue, err := groovyClient.EnsureSingle(source, groovyScriptName, hash, groovyScript)
// then
require.Error(t, err)
assert.True(t, requeue)
err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins)
require.NoError(t, err)
assert.Equal(t, 0, len(jenkins.Status.AppliedGroovyScripts))
})
}
func TestGroovy_Ensure(t *testing.T) {
log.SetupLogger(true)
groovyScript := "groovy-script"
groovyScriptName := "groovy-script-name.groovy"
ctx := context.TODO()
jenkinsName := "jenkins"
namespace := "default"
configMapName := "config-map-name"
secretName := "secret-name"
allGroovyScriptsFunc := func(name string) bool {
return true
}
noUpdateGroovyScript := func(groovyScript string) string {
return groovyScript
}
t.Run("select groovy files with .groovy extension", func(t *testing.T) {
// given
groovyScriptExtension := ".groovy"
jenkins := &v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Name: jenkinsName,
Namespace: namespace,
},
}
customization := v1alpha2.Customization{
Configurations: []v1alpha2.ConfigMapRef{
{
Name: configMapName,
},
},
}
configMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: configMapName,
Namespace: namespace,
},
Data: map[string]string{
groovyScriptName: groovyScript,
"to-omit": "to-omit",
},
}
err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme)
require.NoError(t, err)
fakeClient := fake.NewFakeClient()
err = fakeClient.Create(ctx, jenkins)
require.NoError(t, err)
err = fakeClient.Create(ctx, configMap)
require.NoError(t, err)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
jenkinsClient := jenkinsclient.NewMockJenkins(ctrl)
jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil)
groovyClient := New(jenkinsClient, fakeClient, jenkins, configurationType, customization)
onlyGroovyFilesFunc := func(name string) bool {
return strings.HasSuffix(name, groovyScriptExtension)
}
// when
requeue, err := groovyClient.Ensure(onlyGroovyFilesFunc, noUpdateGroovyScript)
require.NoError(t, err)
assert.True(t, requeue)
requeue, err = groovyClient.Ensure(onlyGroovyFilesFunc, noUpdateGroovyScript)
require.NoError(t, err)
assert.False(t, requeue)
// then
err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins)
require.NoError(t, err)
assert.Equal(t, 1, len(jenkins.Status.AppliedGroovyScripts))
assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[0].ConfigurationType)
assert.Equal(t, "qoXeeh4ia+KXhT01lYNxe+oxByDf8dfT2npP9fgzjbk=", jenkins.Status.AppliedGroovyScripts[0].Hash)
assert.Equal(t, configMapName, jenkins.Status.AppliedGroovyScripts[0].Source)
assert.Equal(t, groovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name)
})
t.Run("change groovy script", func(t *testing.T) {
// given
groovyScriptSuffix := "suffix"
jenkins := &v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Name: jenkinsName,
Namespace: namespace,
},
}
customization := v1alpha2.Customization{
Configurations: []v1alpha2.ConfigMapRef{
{
Name: configMapName,
},
},
}
configMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: configMapName,
Namespace: namespace,
},
Data: map[string]string{
groovyScriptName: groovyScript,
},
}
err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme)
require.NoError(t, err)
fakeClient := fake.NewFakeClient()
err = fakeClient.Create(ctx, jenkins)
require.NoError(t, err)
err = fakeClient.Create(ctx, configMap)
require.NoError(t, err)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
jenkinsClient := jenkinsclient.NewMockJenkins(ctrl)
jenkinsClient.EXPECT().ExecuteScript(groovyScript+groovyScriptSuffix).Return("logs", nil)
groovyClient := New(jenkinsClient, fakeClient, jenkins, configurationType, customization)
updateGroovyFunc := func(groovyScript string) string {
return groovyScript + groovyScriptSuffix
}
// when
requeue, err := groovyClient.Ensure(allGroovyScriptsFunc, updateGroovyFunc)
require.NoError(t, err)
assert.True(t, requeue)
requeue, err = groovyClient.Ensure(allGroovyScriptsFunc, updateGroovyFunc)
require.NoError(t, err)
assert.False(t, requeue)
// then
err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins)
require.NoError(t, err)
assert.Equal(t, 1, len(jenkins.Status.AppliedGroovyScripts))
assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[0].ConfigurationType)
assert.Equal(t, "TgTpV3nDxMNMM93t6jgni0UHa7C+uL+D+BLcW3a7b6M=", jenkins.Status.AppliedGroovyScripts[0].Hash)
assert.Equal(t, configMapName, jenkins.Status.AppliedGroovyScripts[0].Source)
assert.Equal(t, groovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name)
})
t.Run("execute script without secret and save status", func(t *testing.T) {
// given
jenkins := &v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Name: jenkinsName,
Namespace: namespace,
},
}
customization := v1alpha2.Customization{
Configurations: []v1alpha2.ConfigMapRef{
{
Name: configMapName,
},
},
}
configMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: configMapName,
Namespace: namespace,
},
Data: map[string]string{
groovyScriptName: groovyScript,
},
}
err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme)
require.NoError(t, err)
fakeClient := fake.NewFakeClient()
err = fakeClient.Create(ctx, jenkins)
require.NoError(t, err)
err = fakeClient.Create(ctx, configMap)
require.NoError(t, err)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
jenkinsClient := jenkinsclient.NewMockJenkins(ctrl)
jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil)
groovyClient := New(jenkinsClient, fakeClient, jenkins, configurationType, customization)
// when
requeue, err := groovyClient.Ensure(allGroovyScriptsFunc, noUpdateGroovyScript)
require.NoError(t, err)
assert.True(t, requeue)
requeue, err = groovyClient.Ensure(allGroovyScriptsFunc, noUpdateGroovyScript)
require.NoError(t, err)
assert.False(t, requeue)
// then
err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins)
require.NoError(t, err)
assert.Equal(t, 1, len(jenkins.Status.AppliedGroovyScripts))
assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[0].ConfigurationType)
assert.Equal(t, "qoXeeh4ia+KXhT01lYNxe+oxByDf8dfT2npP9fgzjbk=", jenkins.Status.AppliedGroovyScripts[0].Hash)
assert.Equal(t, configMapName, jenkins.Status.AppliedGroovyScripts[0].Source)
assert.Equal(t, groovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name)
})
t.Run("execute script with secret and save status", func(t *testing.T) {
// given
jenkins := &v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Name: jenkinsName,
Namespace: namespace,
},
}
customization := v1alpha2.Customization{
Secret: v1alpha2.SecretRef{Name: secretName},
Configurations: []v1alpha2.ConfigMapRef{
{
Name: configMapName,
},
},
}
configMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: configMapName,
Namespace: namespace,
},
Data: map[string]string{
groovyScriptName: groovyScript,
},
}
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: namespace,
},
Data: map[string][]byte{
"SECRET_KEY": []byte("secret-value"),
},
}
err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme)
require.NoError(t, err)
fakeClient := fake.NewFakeClient()
err = fakeClient.Create(ctx, jenkins)
require.NoError(t, err)
err = fakeClient.Create(ctx, secret)
require.NoError(t, err)
err = fakeClient.Create(ctx, configMap)
require.NoError(t, err)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
jenkinsClient := jenkinsclient.NewMockJenkins(ctrl)
jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil)
groovyClient := New(jenkinsClient, fakeClient, jenkins, configurationType, customization)
// when
requeue, err := groovyClient.Ensure(allGroovyScriptsFunc, noUpdateGroovyScript)
require.NoError(t, err)
assert.True(t, requeue)
requeue, err = groovyClient.Ensure(allGroovyScriptsFunc, noUpdateGroovyScript)
require.NoError(t, err)
assert.False(t, requeue)
// then
err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins)
require.NoError(t, err)
assert.Equal(t, 1, len(jenkins.Status.AppliedGroovyScripts))
assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[0].ConfigurationType)
assert.Equal(t, "em9pjw9mUheUpPRCJWD2Dww+80YQPoHCZbzzKZZw4lo=", jenkins.Status.AppliedGroovyScripts[0].Hash)
assert.Equal(t, configMapName, jenkins.Status.AppliedGroovyScripts[0].Source)
assert.Equal(t, groovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name)
})
}
func TestGroovy_isGroovyScriptAlreadyApplied(t *testing.T) {
log.SetupLogger(true)
emptyCustomization := v1alpha2.Customization{}
t.Run("found", func(t *testing.T) {
jenkins := &v1alpha2.Jenkins{
Status: v1alpha2.JenkinsStatus{
AppliedGroovyScripts: []v1alpha2.AppliedGroovyScript{
{
ConfigurationType: configurationType,
Source: "source",
Name: "name",
Hash: "hash",
},
},
},
}
groovyClient := New(nil, nil, jenkins, configurationType, emptyCustomization)
got := groovyClient.isGroovyScriptAlreadyApplied("source", "name", "hash")
assert.True(t, got)
})
t.Run("not found", func(t *testing.T) {
jenkins := &v1alpha2.Jenkins{
Status: v1alpha2.JenkinsStatus{
AppliedGroovyScripts: []v1alpha2.AppliedGroovyScript{
{
ConfigurationType: configurationType,
Source: "source",
Name: "name",
Hash: "hash",
},
},
},
}
groovyClient := New(nil, nil, jenkins, configurationType, emptyCustomization)
got := groovyClient.isGroovyScriptAlreadyApplied("source", "not-exist", "hash")
assert.False(t, got)
})
t.Run("empty Jenkins status", func(t *testing.T) {
jenkins := &v1alpha2.Jenkins{}
groovyClient := New(nil, nil, jenkins, configurationType, emptyCustomization)
got := groovyClient.isGroovyScriptAlreadyApplied("source", "name", "hash")
assert.False(t, got)
})
}
func TestAddSecretsLoaderToGroovyScript(t *testing.T) {
secretsPath := "/var/jenkins/groovy-scripts-secrets"
secretsLoader := fmt.Sprintf(secretsLoaderGroovyScriptFmt, secretsPath)
t.Run("without imports", func(t *testing.T) {
groovyScript := "println 'Simple groovy script"
updater := AddSecretsLoaderToGroovyScript(secretsPath)
got := updater(groovyScript)
assert.Equal(t, secretsLoader+groovyScript, got)
})
t.Run("with imports", func(t *testing.T) {
groovyScript := `import com.foo.bar
import com.foo.bar2
println 'Simple groovy script'`
imports := `import com.foo.bar
import com.foo.bar2`
tail := `println 'Simple groovy script'`
update := AddSecretsLoaderToGroovyScript(secretsPath)
got := update(groovyScript)
assert.Equal(t, imports+"\n\n"+secretsLoader+"\n\n"+tail, got)
})
t.Run("with imports and separate section", func(t *testing.T) {
groovyScript := `import com.foo.bar
import com.foo.bar2
println 'Simple groovy script'`
imports := `import com.foo.bar
import com.foo.bar2`
tail := `println 'Simple groovy script'`
update := AddSecretsLoaderToGroovyScript(secretsPath)
got := update(groovyScript)
assert.Equal(t, imports+"\n\n"+secretsLoader+"\n\n\n"+tail, got)
})
}
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"reflect"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants"
"github.com/jenkinsci/kubernetes-operator/pkg/constants"
"github.com/jenkinsci/kubernetes-operator/pkg/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+9 -9
View File
@@ -8,15 +8,15 @@ import (
"time"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/user"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/event"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/reason"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/plugins"
jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/client"
"github.com/jenkinsci/kubernetes-operator/pkg/configuration"
"github.com/jenkinsci/kubernetes-operator/pkg/configuration/base"
"github.com/jenkinsci/kubernetes-operator/pkg/configuration/base/resources"
"github.com/jenkinsci/kubernetes-operator/pkg/configuration/user"
"github.com/jenkinsci/kubernetes-operator/pkg/constants"
"github.com/jenkinsci/kubernetes-operator/pkg/notifications/event"
"github.com/jenkinsci/kubernetes-operator/pkg/notifications/reason"
"github.com/jenkinsci/kubernetes-operator/pkg/plugins"
"github.com/jenkinsci/kubernetes-operator/pkg/log"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
@@ -1,34 +0,0 @@
package event
import (
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/reason"
)
// Phase defines the context where notification has been generated: base or user.
type Phase string
// StatusColor is useful for better UX.
type StatusColor string
// LoggingLevel is type for selecting different logging levels.
type LoggingLevel string
// Event contains event details which will be sent as a notification.
type Event struct {
Jenkins v1alpha2.Jenkins
Phase Phase
Level v1alpha2.NotificationLevel
Reason reason.Reason
}
const (
// PhaseBase is core configuration of Jenkins provided by the Operator
PhaseBase Phase = "base"
// PhaseUser is user-defined configuration of Jenkins
PhaseUser Phase = "user"
// PhaseUnknown is untraceable type of configuration
PhaseUnknown Phase = "unknown"
)
@@ -1,123 +0,0 @@
package mailgun
import (
"context"
"fmt"
"strings"
"time"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/event"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/provider"
"github.com/mailgun/mailgun-go/v3"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
)
const (
infoColor = "blue"
warningColor = "red"
defaultColor = "gray"
content = `
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head></head>
<body>
<h1 style="background-color: %s; color: white; padding: 3px 10px;">%s</h1>
<h3>%s</h3>
<table>
<tr>
<td><b>CR name:</b></td>
<td>%s</td>
</tr>
<tr>
<td><b>Phase:</b></td>
<td>%s</td>
</tr>
</table>
<h6 style="font-size: 11px; color: grey; margin-top: 15px;">Powered by Jenkins Operator <3</h6>
</body>
</html>`
)
// MailGun is a sending emails notification service
type MailGun struct {
k8sClient k8sclient.Client
config v1alpha2.Notification
}
// New returns instance of MailGun
func New(k8sClient k8sclient.Client, config v1alpha2.Notification) *MailGun {
return &MailGun{k8sClient: k8sClient, config: config}
}
func (m MailGun) getStatusColor(logLevel v1alpha2.NotificationLevel) event.StatusColor {
switch logLevel {
case v1alpha2.NotificationLevelInfo:
return infoColor
case v1alpha2.NotificationLevelWarning:
return warningColor
default:
return defaultColor
}
}
func (m MailGun) generateMessage(event event.Event) string {
var statusMessage strings.Builder
var reasons string
if m.config.Verbose {
reasons = strings.TrimRight(strings.Join(event.Reason.Verbose(), "</li><li>"), "<li>")
} else {
reasons = strings.TrimRight(strings.Join(event.Reason.Short(), "</li><li>"), "<li>")
}
statusMessage.WriteString("<ul><li>")
statusMessage.WriteString(reasons)
statusMessage.WriteString("</ul>")
statusColor := m.getStatusColor(event.Level)
messageTitle := provider.NotificationTitle(event)
message := statusMessage.String()
crName := event.Jenkins.Name
phase := event.Phase
return fmt.Sprintf(content, statusColor, messageTitle, message, crName, phase)
}
// Send is function for sending directly to API
func (m MailGun) Send(event event.Event) error {
secret := &corev1.Secret{}
selector := m.config.Mailgun.APIKeySecretKeySelector
err := m.k8sClient.Get(context.TODO(),
types.NamespacedName{Name: selector.Name, Namespace: event.Jenkins.Namespace}, secret)
if err != nil {
return errors.WithStack(err)
}
secretValue := string(secret.Data[selector.Key])
if secretValue == "" {
return errors.Errorf("Mailgun API secret is empty in secret '%s/%s[%s]",
event.Jenkins.Namespace, selector.Name, selector.Key)
}
mg := mailgun.NewMailgun(m.config.Mailgun.Domain, secretValue)
from := fmt.Sprintf("Jenkins Operator Notifier <%s>", m.config.Mailgun.From)
subject := provider.NotificationTitle(event)
recipient := m.config.Mailgun.Recipient
msg := mg.NewMessage(from, subject, "", recipient)
msg.SetHtml(m.generateMessage(event))
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
_, _, err = mg.Send(ctx, msg)
return err
}
@@ -1,182 +0,0 @@
package mailgun
import (
"fmt"
"strings"
"testing"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/event"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/provider"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/reason"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
func TestGenerateMessages(t *testing.T) {
t.Run("happy", func(t *testing.T) {
crName := "test-jenkins"
crNamespace := "test-namespace"
phase := event.PhaseBase
level := v1alpha2.NotificationLevelInfo
res := reason.NewUndefined(reason.KubernetesSource, []string{"test-string"}, "test-verbose")
s := MailGun{
k8sClient: fake.NewFakeClient(),
config: v1alpha2.Notification{
Verbose: true,
},
}
e := event.Event{
Jenkins: v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Name: crName,
Namespace: crNamespace,
},
},
Phase: phase,
Level: level,
Reason: res,
}
var statusMessage strings.Builder
r := strings.TrimRight(strings.Join(e.Reason.Verbose(), "</li><li>"), "<li>")
statusMessage.WriteString("<ul><li>")
statusMessage.WriteString(r)
statusMessage.WriteString("</ul>")
want := s.generateMessage(e)
got := fmt.Sprintf(content, s.getStatusColor(e.Level),
provider.NotificationTitle(e), statusMessage.String(), e.Jenkins.Name, e.Phase)
assert.Equal(t, want, got)
})
t.Run("with nils", func(t *testing.T) {
crName := "nil"
crNamespace := "nil"
phase := event.PhaseBase
level := v1alpha2.NotificationLevelInfo
res := reason.NewUndefined(reason.KubernetesSource, []string{"nil"}, "nil")
s := MailGun{
k8sClient: fake.NewFakeClient(),
config: v1alpha2.Notification{
Verbose: true,
},
}
e := event.Event{
Jenkins: v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Name: crName,
Namespace: crNamespace,
},
},
Phase: phase,
Level: level,
Reason: res,
}
var statusMessage strings.Builder
r := strings.TrimRight(strings.Join(e.Reason.Verbose(), "</li><li>"), "<li>")
statusMessage.WriteString("<ul><li>")
statusMessage.WriteString(r)
statusMessage.WriteString("</ul>")
want := s.generateMessage(e)
got := fmt.Sprintf(content, s.getStatusColor(e.Level),
provider.NotificationTitle(e), statusMessage.String(), e.Jenkins.Name, e.Phase)
assert.Equal(t, want, got)
})
t.Run("with empty strings", func(t *testing.T) {
crName := ""
crNamespace := ""
phase := event.PhaseBase
level := v1alpha2.NotificationLevelInfo
res := reason.NewUndefined(reason.KubernetesSource, []string{""}, "")
s := MailGun{
k8sClient: fake.NewFakeClient(),
config: v1alpha2.Notification{
Verbose: true,
},
}
e := event.Event{
Jenkins: v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Name: crName,
Namespace: crNamespace,
},
},
Phase: phase,
Level: level,
Reason: res,
}
var statusMessage strings.Builder
r := strings.TrimRight(strings.Join(e.Reason.Verbose(), "</li><li>"), "<li>")
statusMessage.WriteString("<ul><li>")
statusMessage.WriteString(r)
statusMessage.WriteString("</ul>")
want := s.generateMessage(e)
got := fmt.Sprintf(content, s.getStatusColor(e.Level),
provider.NotificationTitle(e), statusMessage.String(), e.Jenkins.Name, e.Phase)
assert.Equal(t, want, got)
})
t.Run("with utf-8 characters", func(t *testing.T) {
crName := "ąśćńółżź"
crNamespace := "ąśćńółżź"
phase := event.PhaseBase
level := v1alpha2.NotificationLevelInfo
res := reason.NewUndefined(reason.KubernetesSource, []string{"ąśćńółżź"}, "ąśćńółżź")
s := MailGun{
k8sClient: fake.NewFakeClient(),
config: v1alpha2.Notification{
Verbose: true,
},
}
e := event.Event{
Jenkins: v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Name: crName,
Namespace: crNamespace,
},
},
Phase: phase,
Level: level,
Reason: res,
}
var statusMessage strings.Builder
r := strings.TrimRight(strings.Join(e.Reason.Verbose(), "</li><li>"), "<li>")
statusMessage.WriteString("<ul><li>")
statusMessage.WriteString(r)
statusMessage.WriteString("</ul>")
want := s.generateMessage(e)
got := fmt.Sprintf(content, s.getStatusColor(e.Level),
provider.NotificationTitle(e), statusMessage.String(), e.Jenkins.Name, e.Phase)
assert.Equal(t, want, got)
})
}
@@ -1,147 +0,0 @@
package msteams
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/event"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/provider"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
)
const (
infoColor = "439FE0"
warningColor = "E81123"
defaultColor = "C8C8C8"
)
// Teams is a Microsoft MicrosoftTeams notification service
type Teams struct {
httpClient http.Client
k8sClient k8sclient.Client
config v1alpha2.Notification
}
// New returns instance of Teams
func New(k8sClient k8sclient.Client, config v1alpha2.Notification, httpClient http.Client) *Teams {
return &Teams{k8sClient: k8sClient, config: config, httpClient: httpClient}
}
// Message is representation of json message structure
type Message struct {
Type string `json:"@type"`
Context string `json:"@context"`
ThemeColor event.StatusColor `json:"themeColor"`
Title string `json:"title"`
Sections []Section `json:"sections"`
Summary string `json:"summary"`
}
// Section is MS Teams message section
type Section struct {
Facts []Fact `json:"facts"`
Text string `json:"text"`
}
// Fact is field where we can put content
type Fact struct {
Name string `json:"name"`
Value string `json:"value"`
}
func (t Teams) getStatusColor(logLevel v1alpha2.NotificationLevel) event.StatusColor {
switch logLevel {
case v1alpha2.NotificationLevelInfo:
return infoColor
case v1alpha2.NotificationLevelWarning:
return warningColor
default:
return defaultColor
}
}
func (t Teams) generateMessage(e event.Event) Message {
var reason string
if t.config.Verbose {
reason = strings.Join(e.Reason.Verbose(), "\n\n - ")
} else {
reason = strings.Join(e.Reason.Short(), "\n\n - ")
}
tm := Message{
Title: provider.NotificationTitle(e),
Type: "MessageCard",
Context: "https://schema.org/extensions",
ThemeColor: t.getStatusColor(e.Level),
Sections: []Section{
{
Facts: []Fact{
{
Name: provider.CrNameFieldName,
Value: e.Jenkins.Name,
},
{
Name: provider.NamespaceFieldName,
Value: e.Jenkins.Namespace,
},
{
Name: provider.PhaseFieldName,
Value: string(e.Phase),
},
},
Text: reason,
},
},
Summary: reason,
}
return tm
}
// Send is function for sending directly to API
func (t Teams) Send(e event.Event) error {
secret := &corev1.Secret{}
selector := t.config.Teams.WebHookURLSecretKeySelector
err := t.k8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: e.Jenkins.Namespace}, secret)
if err != nil {
return errors.WithStack(err)
}
secretValue := string(secret.Data[selector.Key])
if secretValue == "" {
return errors.Errorf("Microsoft Teams WebHook URL is empty in secret '%s/%s[%s]", e.Jenkins.Namespace, selector.Name, selector.Key)
}
msg, err := json.Marshal(t.generateMessage(e))
if err != nil {
return errors.WithStack(err)
}
request, err := http.NewRequest("POST", secretValue, bytes.NewBuffer(msg))
if err != nil {
return errors.WithStack(err)
}
resp, err := t.httpClient.Do(request)
if err != nil {
return errors.WithStack(err)
}
if resp.StatusCode != http.StatusOK {
return errors.New(fmt.Sprintf("Invalid response from server: %s", resp.Status))
}
defer func() { _ = resp.Body.Close() }()
return nil
}
@@ -1,287 +0,0 @@
package msteams
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/event"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/provider"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/reason"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
var (
testPhase = event.PhaseUser
testCrName = "test-cr"
testNamespace = "default"
testReason = reason.NewPodRestart(
reason.KubernetesSource,
[]string{"test-reason-1"},
[]string{"test-verbose-1"}...,
)
testLevel = v1alpha2.NotificationLevelWarning
)
func TestTeams_Send(t *testing.T) {
fakeClient := fake.NewFakeClient()
testURLSelectorKeyName := "test-url-selector"
testSecretName := "test-secret"
event := event.Event{
Jenkins: v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Name: testCrName,
Namespace: testNamespace,
},
},
Phase: testPhase,
Level: testLevel,
Reason: testReason,
}
teams := Teams{k8sClient: fakeClient, config: v1alpha2.Notification{
Teams: &v1alpha2.MicrosoftTeams{
WebHookURLSecretKeySelector: v1alpha2.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: testSecretName,
},
Key: testURLSelectorKeyName,
},
},
}}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var message Message
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&message)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, message.Title, provider.NotificationTitle(event))
assert.Equal(t, message.ThemeColor, teams.getStatusColor(event.Level))
mainSection := message.Sections[0]
reason := strings.Join(event.Reason.Short(), "\n\n - ")
assert.Equal(t, mainSection.Text, reason)
for _, fact := range mainSection.Facts {
switch fact.Name {
case provider.PhaseFieldName:
assert.Equal(t, fact.Value, string(event.Phase))
case provider.CrNameFieldName:
assert.Equal(t, fact.Value, event.Jenkins.Name)
case provider.MessageFieldName:
assert.Equal(t, fact.Value, reason)
case provider.LevelFieldName:
assert.Equal(t, fact.Value, string(event.Level))
case provider.NamespaceFieldName:
assert.Equal(t, fact.Value, event.Jenkins.Namespace)
default:
t.Errorf("Found unexpected '%+v' fact", fact)
}
}
}))
defer server.Close()
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: testSecretName,
Namespace: testNamespace,
},
Data: map[string][]byte{
testURLSelectorKeyName: []byte(server.URL),
},
}
err := fakeClient.Create(context.TODO(), secret)
assert.NoError(t, err)
err = teams.Send(event)
assert.NoError(t, err)
}
func TestGenerateMessages(t *testing.T) {
t.Run("happy", func(t *testing.T) {
crName := "test-jenkins"
crNamespace := "test-namespace"
phase := event.PhaseBase
level := v1alpha2.NotificationLevelInfo
res := reason.NewUndefined(reason.KubernetesSource, []string{"test-string"}, "test-verbose")
s := Teams{
httpClient: http.Client{},
k8sClient: fake.NewFakeClient(),
config: v1alpha2.Notification{
Verbose: true,
},
}
e := event.Event{
Jenkins: v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Name: crName,
Namespace: crNamespace,
},
},
Phase: phase,
Level: level,
Reason: res,
}
message := s.generateMessage(e)
msg := strings.Join(e.Reason.Verbose(), "\n\n - ")
mainSection := message.Sections[0]
crNameFact := mainSection.Facts[0]
namespaceFact := mainSection.Facts[1]
phaseFact := mainSection.Facts[2]
assert.Equal(t, mainSection.Text, msg)
assert.Equal(t, crNameFact.Value, e.Jenkins.Name)
assert.Equal(t, namespaceFact.Value, e.Jenkins.Namespace)
assert.Equal(t, event.Phase(phaseFact.Value), e.Phase)
})
t.Run("with nils", func(t *testing.T) {
crName := "nil"
crNamespace := "nil"
phase := event.PhaseBase
level := v1alpha2.NotificationLevelInfo
res := reason.NewUndefined(reason.KubernetesSource, []string{"nil"}, "nil")
s := Teams{
httpClient: http.Client{},
k8sClient: fake.NewFakeClient(),
config: v1alpha2.Notification{
Verbose: true,
},
}
e := event.Event{
Jenkins: v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Name: crName,
Namespace: crNamespace,
},
},
Phase: phase,
Level: level,
Reason: res,
}
message := s.generateMessage(e)
msg := strings.Join(e.Reason.Verbose(), "\n\n - ")
mainSection := message.Sections[0]
crNameFact := mainSection.Facts[0]
namespaceFact := mainSection.Facts[1]
phaseFact := mainSection.Facts[2]
assert.Equal(t, mainSection.Text, msg)
assert.Equal(t, crNameFact.Value, e.Jenkins.Name)
assert.Equal(t, namespaceFact.Value, e.Jenkins.Namespace)
assert.Equal(t, event.Phase(phaseFact.Value), e.Phase)
})
t.Run("with empty strings", func(t *testing.T) {
crName := ""
crNamespace := ""
phase := event.PhaseBase
level := v1alpha2.NotificationLevelInfo
res := reason.NewUndefined(reason.KubernetesSource, []string{""}, "")
s := Teams{
httpClient: http.Client{},
k8sClient: fake.NewFakeClient(),
config: v1alpha2.Notification{
Verbose: true,
},
}
e := event.Event{
Jenkins: v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Name: crName,
Namespace: crNamespace,
},
},
Phase: phase,
Level: level,
Reason: res,
}
message := s.generateMessage(e)
msg := strings.Join(e.Reason.Verbose(), "\n\n - ")
mainSection := message.Sections[0]
crNameFact := mainSection.Facts[0]
namespaceFact := mainSection.Facts[1]
phaseFact := mainSection.Facts[2]
assert.Equal(t, mainSection.Text, msg)
assert.Equal(t, crNameFact.Value, e.Jenkins.Name)
assert.Equal(t, namespaceFact.Value, e.Jenkins.Namespace)
assert.Equal(t, event.Phase(phaseFact.Value), e.Phase)
})
t.Run("with utf-8 characters", func(t *testing.T) {
crName := "ąśćńółżź"
crNamespace := "ąśćńółżź"
phase := event.PhaseBase
level := v1alpha2.NotificationLevelInfo
res := reason.NewUndefined(reason.KubernetesSource, []string{"ąśćńółżź"}, "ąśćńółżź")
s := Teams{
httpClient: http.Client{},
k8sClient: fake.NewFakeClient(),
config: v1alpha2.Notification{
Verbose: true,
},
}
e := event.Event{
Jenkins: v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Name: crName,
Namespace: crNamespace,
},
},
Phase: phase,
Level: level,
Reason: res,
}
message := s.generateMessage(e)
msg := strings.Join(e.Reason.Verbose(), "\n\n - ")
mainSection := message.Sections[0]
crNameFact := mainSection.Facts[0]
namespaceFact := mainSection.Facts[1]
phaseFact := mainSection.Facts[2]
assert.Equal(t, mainSection.Text, msg)
assert.Equal(t, crNameFact.Value, e.Jenkins.Name)
assert.Equal(t, namespaceFact.Value, e.Jenkins.Namespace)
assert.Equal(t, event.Phase(phaseFact.Value), e.Phase)
})
}
@@ -1,41 +0,0 @@
package provider
import (
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/event"
)
const (
// InfoTitleText is info header of notification
InfoTitleText = "Jenkins Operator reconciliation info"
// WarnTitleText is warning header of notification
WarnTitleText = "Jenkins Operator reconciliation warning"
// MessageFieldName is field title for message content
MessageFieldName = "Message"
// LevelFieldName is field title for level enum
LevelFieldName = "Level"
// CrNameFieldName is field title for CR Name string
CrNameFieldName = "CR Name"
// PhaseFieldName is field title for Phase enum
PhaseFieldName = "Phase"
// NamespaceFieldName is field title for Namespace string
NamespaceFieldName = "Namespace"
)
// NotificationTitle converts NotificationLevel enum to string
func NotificationTitle(event event.Event) string {
switch event.Level {
case v1alpha2.NotificationLevelInfo:
return InfoTitleText
case v1alpha2.NotificationLevelWarning:
return WarnTitleText
default:
return ""
}
}
@@ -1,198 +0,0 @@
package reason
import "fmt"
const (
// OperatorSource defines that notification concerns operator
OperatorSource Source = "operator"
// KubernetesSource defines that notification concerns kubernetes
KubernetesSource Source = "kubernetes"
// HumanSource defines that notification concerns human
HumanSource Source = "human"
)
// Reason is interface that let us know why operator sent notification.
type Reason interface {
Short() []string
Verbose() []string
HasMessages() bool
}
// Undefined is base or untraceable reason.
type Undefined struct {
source Source
short []string
verbose []string
}
// PodRestart defines the reason why Jenkins master pod restarted.
type PodRestart struct {
Undefined
}
// PodCreation informs that pod is being created.
type PodCreation struct {
Undefined
}
// ReconcileLoopFailed defines the reason why the reconcile loop failed.
type ReconcileLoopFailed struct {
Undefined
}
// GroovyScriptExecutionFailed defines the reason why the groovy script execution failed.
type GroovyScriptExecutionFailed struct {
Undefined
}
// BaseConfigurationFailed defines the reason why base configuration phase failed.
type BaseConfigurationFailed struct {
Undefined
}
// BaseConfigurationComplete informs that base configuration is valid and complete.
type BaseConfigurationComplete struct {
Undefined
}
// UserConfigurationFailed defines the reason why user configuration phase failed.
type UserConfigurationFailed struct {
Undefined
}
// UserConfigurationComplete informs that user configuration is valid and complete.
type UserConfigurationComplete struct {
Undefined
}
// NewUndefined returns new instance of Undefined.
func NewUndefined(source Source, short []string, verbose ...string) *Undefined {
return &Undefined{source: source, short: short, verbose: checkIfVerboseEmpty(short, verbose)}
}
// NewPodRestart returns new instance of PodRestart.
func NewPodRestart(source Source, short []string, verbose ...string) *PodRestart {
restartPodMessage := fmt.Sprintf("Jenkins master pod restarted by %s:", source)
if len(short) == 1 {
short = []string{fmt.Sprintf("%s %s", restartPodMessage, short[0])}
} else if len(short) > 1 {
short = append([]string{restartPodMessage}, short...)
}
if len(verbose) == 1 {
verbose = []string{fmt.Sprintf("%s %s", restartPodMessage, verbose[0])}
} else if len(verbose) > 1 {
verbose = append([]string{restartPodMessage}, verbose...)
}
return &PodRestart{
Undefined{
source: source,
short: short,
verbose: checkIfVerboseEmpty(short, verbose),
},
}
}
// NewPodCreation returns new instance of PodCreation.
func NewPodCreation(source Source, short []string, verbose ...string) *PodCreation {
return &PodCreation{
Undefined{
source: source,
short: short,
verbose: checkIfVerboseEmpty(short, verbose),
},
}
}
// NewReconcileLoopFailed returns new instance of ReconcileLoopFailed.
func NewReconcileLoopFailed(source Source, short []string, verbose ...string) *ReconcileLoopFailed {
return &ReconcileLoopFailed{
Undefined{
source: source,
short: short,
verbose: checkIfVerboseEmpty(short, verbose),
},
}
}
// NewGroovyScriptExecutionFailed returns new instance of GroovyScriptExecutionFailed.
func NewGroovyScriptExecutionFailed(source Source, short []string, verbose ...string) *GroovyScriptExecutionFailed {
return &GroovyScriptExecutionFailed{
Undefined{
source: source,
short: short,
verbose: checkIfVerboseEmpty(short, verbose),
},
}
}
// NewBaseConfigurationFailed returns new instance of BaseConfigurationFailed.
func NewBaseConfigurationFailed(source Source, short []string, verbose ...string) *BaseConfigurationFailed {
return &BaseConfigurationFailed{
Undefined{
source: source,
short: short,
verbose: checkIfVerboseEmpty(short, verbose),
},
}
}
// NewBaseConfigurationComplete returns new instance of BaseConfigurationComplete.
func NewBaseConfigurationComplete(source Source, short []string, verbose ...string) *BaseConfigurationComplete {
return &BaseConfigurationComplete{
Undefined{
source: source,
short: short,
verbose: checkIfVerboseEmpty(short, verbose),
},
}
}
// NewUserConfigurationFailed returns new instance of UserConfigurationFailed.
func NewUserConfigurationFailed(source Source, short []string, verbose ...string) *UserConfigurationFailed {
return &UserConfigurationFailed{
Undefined{
source: source,
short: short,
verbose: checkIfVerboseEmpty(short, verbose),
},
}
}
// NewUserConfigurationComplete returns new instance of UserConfigurationComplete.
func NewUserConfigurationComplete(source Source, short []string, verbose ...string) *UserConfigurationComplete {
return &UserConfigurationComplete{
Undefined{
source: source,
short: short,
verbose: checkIfVerboseEmpty(short, verbose),
},
}
}
// Source is enum type that informs us what triggered notification.
type Source string
// Short is list of reasons.
func (p Undefined) Short() []string {
return p.short
}
// Verbose is list of reasons with details.
func (p Undefined) Verbose() []string {
return p.verbose
}
// HasMessages checks if there is any message.
func (p Undefined) HasMessages() bool {
return len(p.short) > 0 || len(p.verbose) > 0
}
func checkIfVerboseEmpty(short []string, verbose []string) []string {
if len(verbose) == 0 {
return short
}
return verbose
}
@@ -1,77 +0,0 @@
package reason
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestCopyToVerboseIfNil(t *testing.T) {
t.Run("happy", func(t *testing.T) {
var verbose []string
short := []string{"test", "string"}
assert.Equal(t, checkIfVerboseEmpty(short, verbose), short)
})
t.Run("check with invalid slice", func(t *testing.T) {
valid := []string{"valid", "string"}
invalid := []string{"invalid", "string"}
assert.Equal(t, checkIfVerboseEmpty(invalid, valid), valid)
})
t.Run("check with two empty slices", func(t *testing.T) {
var short []string
var verbose []string
assert.Equal(t, checkIfVerboseEmpty(short, verbose), verbose)
})
t.Run("with nils", func(t *testing.T) {
assert.Equal(t, checkIfVerboseEmpty(nil, nil), []string(nil))
})
}
func TestUndefined_HasMessages(t *testing.T) {
t.Run("short full", func(t *testing.T) {
podRestart := NewUndefined(KubernetesSource, []string{"test", "another-test"})
assert.True(t, podRestart.HasMessages())
})
t.Run("verbose full", func(t *testing.T) {
podRestart := NewUndefined(KubernetesSource, []string{}, []string{"test", "another-test"}...)
assert.True(t, podRestart.HasMessages())
})
t.Run("short empty", func(t *testing.T) {
podRestart := NewUndefined(KubernetesSource, []string{})
assert.False(t, podRestart.HasMessages())
})
t.Run("verbose and short full", func(t *testing.T) {
podRestart := NewUndefined(KubernetesSource, []string{"test", "another-test"}, []string{"test", "another-test"}...)
assert.True(t, podRestart.HasMessages())
})
t.Run("verbose and short empty", func(t *testing.T) {
podRestart := NewUndefined(KubernetesSource, []string{}, []string{}...)
assert.False(t, podRestart.HasMessages())
})
}
func TestPodRestartPrepend(t *testing.T) {
t.Run("happy with one message", func(t *testing.T) {
res := "test-reason"
podRestart := NewPodRestart(KubernetesSource, []string{res})
assert.Equal(t, fmt.Sprintf("Jenkins master pod restarted by %s: %s", KubernetesSource, res), podRestart.short[0])
})
t.Run("happy with multiple message", func(t *testing.T) {
podRestart := NewPodRestart(KubernetesSource, []string{"first-reason", "second-reason", "third-reason"})
assert.Equal(t, fmt.Sprintf("Jenkins master pod restarted by %s:", KubernetesSource), podRestart.short[0])
})
}
@@ -1,92 +0,0 @@
package notifications
import (
"fmt"
"net/http"
"reflect"
"strings"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/event"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/mailgun"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/msteams"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/slack"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/smtp"
k8sevent "github.com/jenkinsci/kubernetes-operator/pkg/event"
"github.com/jenkinsci/kubernetes-operator/pkg/log"
"github.com/pkg/errors"
k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
)
// Provider is the communication service handler.
type Provider interface {
Send(event event.Event) error
}
// Listen listens for incoming events and send it as notifications.
func Listen(events chan event.Event, k8sEvent k8sevent.Recorder, k8sClient k8sclient.Client) {
httpClient := http.Client{}
for e := range events {
logger := log.Log.WithValues("cr", e.Jenkins.Name)
if !e.Reason.HasMessages() {
logger.V(log.VWarn).Info("Reason has no messages, this should not happen")
continue // skip empty messages
}
k8sEvent.Emit(&e.Jenkins,
eventLevelToKubernetesEventType(e.Level),
k8sevent.Reason(reflect.TypeOf(e.Reason).Name()),
strings.Join(e.Reason.Short(), "; "),
)
for _, notificationConfig := range e.Jenkins.Spec.Notifications {
var err error
var provider Provider
switch {
case notificationConfig.Slack != nil:
provider = slack.New(k8sClient, notificationConfig, httpClient)
case notificationConfig.Teams != nil:
provider = msteams.New(k8sClient, notificationConfig, httpClient)
case notificationConfig.Mailgun != nil:
provider = mailgun.New(k8sClient, notificationConfig)
case notificationConfig.SMTP != nil:
provider = smtp.New(k8sClient, notificationConfig)
default:
logger.V(log.VWarn).Info(fmt.Sprintf("Unknown notification service `%+v`", notificationConfig))
continue
}
isInfoEvent := e.Level == v1alpha2.NotificationLevelInfo
wantsWarning := notificationConfig.LoggingLevel == v1alpha2.NotificationLevelWarning
if isInfoEvent && wantsWarning {
continue // skip the event
}
go func(notificationConfig v1alpha2.Notification) {
err = provider.Send(e)
if err != nil {
wrapped := errors.WithMessage(err,
fmt.Sprintf("failed to send notification '%s'", notificationConfig.Name))
if log.Debug {
logger.Error(nil, fmt.Sprintf("%+v", wrapped))
} else {
logger.Error(nil, fmt.Sprintf("%s", wrapped))
}
}
}(notificationConfig)
}
}
}
func eventLevelToKubernetesEventType(level v1alpha2.NotificationLevel) k8sevent.Type {
switch level {
case v1alpha2.NotificationLevelWarning:
return k8sevent.TypeWarning
case v1alpha2.NotificationLevelInfo:
return k8sevent.TypeNormal
default:
return k8sevent.TypeNormal
}
}
@@ -1,152 +0,0 @@
package slack
import (
"bytes"
"context"
"encoding/json"
"net/http"
"strings"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/event"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/provider"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
)
const (
infoColor = "#439FE0"
warningColor = "danger"
defaultColor = "#c8c8c8"
)
// Slack is a Slack notification service.
type Slack struct {
httpClient http.Client
k8sClient k8sclient.Client
config v1alpha2.Notification
}
// New returns instance of Slack.
func New(k8sClient k8sclient.Client, config v1alpha2.Notification, httpClient http.Client) *Slack {
return &Slack{k8sClient: k8sClient, config: config, httpClient: httpClient}
}
// Message is representation of json message.
type Message struct {
Text string `json:"text"`
Attachments []Attachment `json:"attachments"`
}
// Attachment is representation of json attachment.
type Attachment struct {
Fallback string `json:"fallback"`
Color event.StatusColor `json:"color"`
Pretext string `json:"pretext"`
Title string `json:"title"`
Text string `json:"text"`
Fields []Field `json:"fields"`
Footer string `json:"footer"`
}
// Field is representation of json field.
type Field struct {
Title string `json:"title"`
Value string `json:"value"`
Short bool `json:"short"`
}
func (s Slack) getStatusColor(logLevel v1alpha2.NotificationLevel) event.StatusColor {
switch logLevel {
case v1alpha2.NotificationLevelInfo:
return infoColor
case v1alpha2.NotificationLevelWarning:
return warningColor
default:
return defaultColor
}
}
func (s Slack) generateMessage(e event.Event) Message {
var messageStringBuilder strings.Builder
if s.config.Verbose {
for _, msg := range e.Reason.Verbose() {
messageStringBuilder.WriteString("\n - " + msg + "\n")
}
} else {
for _, msg := range e.Reason.Short() {
messageStringBuilder.WriteString("\n - " + msg + "\n")
}
}
sm := Message{
Attachments: []Attachment{
{
Title: provider.NotificationTitle(e),
Fallback: "",
Color: s.getStatusColor(e.Level),
Fields: []Field{
{
Title: "",
Value: messageStringBuilder.String(),
Short: false,
},
{
Title: provider.NamespaceFieldName,
Value: e.Jenkins.Namespace,
Short: true,
},
{
Title: provider.CrNameFieldName,
Value: e.Jenkins.Name,
Short: true,
},
{
Title: provider.PhaseFieldName,
Value: string(e.Phase),
Short: true,
},
},
},
},
}
return sm
}
// Send is function for sending directly to API.
func (s Slack) Send(e event.Event) error {
secret := &corev1.Secret{}
selector := s.config.Slack.WebHookURLSecretKeySelector
err := s.k8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: e.Jenkins.Namespace}, secret)
if err != nil {
return err
}
slackMessage, err := json.Marshal(s.generateMessage(e))
if err != nil {
return err
}
secretValue := string(secret.Data[selector.Key])
if secretValue == "" {
return errors.Errorf("Slack WebHook URL is empty in secret '%s/%s[%s]", e.Jenkins.Namespace, selector.Name, selector.Key)
}
request, err := http.NewRequest("POST", secretValue, bytes.NewBuffer(slackMessage))
if err != nil {
return err
}
resp, err := s.httpClient.Do(request)
if err != nil {
return err
}
defer func() { _ = resp.Body.Close() }()
return nil
}
@@ -1,303 +0,0 @@
package slack
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/event"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/provider"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/reason"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
var (
testPhase = event.PhaseUser
testCrName = "test-cr"
testNamespace = "default"
testReason = reason.NewPodRestart(
reason.KubernetesSource,
[]string{"test-reason-1"},
[]string{"test-verbose-1"}...,
)
testLevel = v1alpha2.NotificationLevelWarning
)
func TestSlack_Send(t *testing.T) {
fakeClient := fake.NewFakeClient()
testURLSelectorKeyName := "test-url-selector"
testSecretName := "test-secret"
e := event.Event{
Jenkins: v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Name: testCrName,
Namespace: testNamespace,
},
},
Phase: testPhase,
Level: testLevel,
Reason: testReason,
}
slack := Slack{k8sClient: fakeClient, config: v1alpha2.Notification{
Slack: &v1alpha2.Slack{
WebHookURLSecretKeySelector: v1alpha2.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: testSecretName,
},
Key: testURLSelectorKeyName,
},
},
}}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var message Message
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&message)
if err != nil {
t.Fatal(err)
}
mainAttachment := message.Attachments[0]
assert.Equal(t, mainAttachment.Title, provider.NotificationTitle(e))
for _, field := range mainAttachment.Fields {
switch field.Title {
case provider.PhaseFieldName:
assert.Equal(t, field.Value, string(e.Phase))
case provider.CrNameFieldName:
assert.Equal(t, field.Value, e.Jenkins.Name)
case "":
message := ""
for _, msg := range e.Reason.Short() {
message = message + "\n - " + msg + "\n"
}
assert.Equal(t, field.Value, message)
case provider.LevelFieldName:
assert.Equal(t, field.Value, string(e.Level))
case provider.NamespaceFieldName:
assert.Equal(t, field.Value, e.Jenkins.Namespace)
default:
t.Errorf("Unexpected field %+v", field)
}
}
assert.Equal(t, mainAttachment.Footer, "")
assert.Equal(t, mainAttachment.Color, slack.getStatusColor(e.Level))
}))
defer server.Close()
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: testSecretName,
Namespace: testNamespace,
},
Data: map[string][]byte{
testURLSelectorKeyName: []byte(server.URL),
},
}
err := fakeClient.Create(context.TODO(), secret)
assert.NoError(t, err)
err = slack.Send(e)
assert.NoError(t, err)
}
func TestGenerateMessage(t *testing.T) {
t.Run("happy", func(t *testing.T) {
crName := "test-jenkins"
crNamespace := "test-namespace"
phase := event.PhaseBase
level := v1alpha2.NotificationLevelInfo
res := reason.NewUndefined(reason.KubernetesSource, []string{"test-string"}, "test-verbose")
s := Slack{
httpClient: http.Client{},
k8sClient: fake.NewFakeClient(),
config: v1alpha2.Notification{
Verbose: true,
},
}
e := event.Event{
Jenkins: v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Name: crName,
Namespace: crNamespace,
},
},
Phase: phase,
Level: level,
Reason: res,
}
message := s.generateMessage(e)
var messageStringBuilder strings.Builder
for _, msg := range e.Reason.Verbose() {
messageStringBuilder.WriteString("\n - " + msg + "\n")
}
mainAttachment := message.Attachments[0]
messageField := mainAttachment.Fields[0]
namespaceField := mainAttachment.Fields[1]
crNameField := mainAttachment.Fields[2]
phaseField := mainAttachment.Fields[3]
assert.Equal(t, messageField.Value, messageStringBuilder.String())
assert.Equal(t, namespaceField.Value, e.Jenkins.Namespace)
assert.Equal(t, crNameField.Value, e.Jenkins.Name)
assert.Equal(t, event.Phase(phaseField.Value), e.Phase)
})
t.Run("with nils", func(t *testing.T) {
crName := "nil"
crNamespace := "nil"
phase := event.PhaseBase
level := v1alpha2.NotificationLevelInfo
res := reason.NewUndefined(reason.KubernetesSource, []string{"nil"}, "nil")
s := Slack{
httpClient: http.Client{},
k8sClient: fake.NewFakeClient(),
config: v1alpha2.Notification{
Verbose: true,
},
}
e := event.Event{
Jenkins: v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Name: crName,
Namespace: crNamespace,
},
},
Phase: phase,
Level: level,
Reason: res,
}
message := s.generateMessage(e)
var messageStringBuilder strings.Builder
for _, msg := range e.Reason.Verbose() {
messageStringBuilder.WriteString("\n - " + msg + "\n")
}
mainAttachment := message.Attachments[0]
messageField := mainAttachment.Fields[0]
namespaceField := mainAttachment.Fields[1]
crNameField := mainAttachment.Fields[2]
phaseField := mainAttachment.Fields[3]
assert.Equal(t, messageField.Value, messageStringBuilder.String())
assert.Equal(t, namespaceField.Value, e.Jenkins.Namespace)
assert.Equal(t, crNameField.Value, e.Jenkins.Name)
assert.Equal(t, event.Phase(phaseField.Value), e.Phase)
})
t.Run("with empty strings", func(t *testing.T) {
crName := ""
crNamespace := ""
phase := event.PhaseBase
level := v1alpha2.NotificationLevelInfo
res := reason.NewUndefined(reason.KubernetesSource, []string{""}, "")
s := Slack{
httpClient: http.Client{},
k8sClient: fake.NewFakeClient(),
config: v1alpha2.Notification{
Verbose: true,
},
}
e := event.Event{
Jenkins: v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Name: crName,
Namespace: crNamespace,
},
},
Phase: phase,
Level: level,
Reason: res,
}
message := s.generateMessage(e)
var messageStringBuilder strings.Builder
for _, msg := range e.Reason.Verbose() {
messageStringBuilder.WriteString("\n - " + msg + "\n")
}
mainAttachment := message.Attachments[0]
messageField := mainAttachment.Fields[0]
namespaceField := mainAttachment.Fields[1]
crNameField := mainAttachment.Fields[2]
phaseField := mainAttachment.Fields[3]
assert.Equal(t, messageField.Value, messageStringBuilder.String())
assert.Equal(t, namespaceField.Value, e.Jenkins.Namespace)
assert.Equal(t, crNameField.Value, e.Jenkins.Name)
assert.Equal(t, event.Phase(phaseField.Value), e.Phase)
})
t.Run("with utf-8 characters", func(t *testing.T) {
crName := "ąśćńółżź"
crNamespace := "ąśćńółżź"
phase := event.PhaseBase
level := v1alpha2.NotificationLevelInfo
res := reason.NewUndefined(reason.KubernetesSource, []string{"ąśćńółżź"}, "ąśćńółżź")
s := Slack{
httpClient: http.Client{},
k8sClient: fake.NewFakeClient(),
config: v1alpha2.Notification{
Verbose: true,
},
}
e := event.Event{
Jenkins: v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Name: crName,
Namespace: crNamespace,
},
},
Phase: phase,
Level: level,
Reason: res,
}
message := s.generateMessage(e)
var messageStringBuilder strings.Builder
for _, msg := range e.Reason.Verbose() {
messageStringBuilder.WriteString("\n - " + msg + "\n")
}
mainAttachment := message.Attachments[0]
messageField := mainAttachment.Fields[0]
namespaceField := mainAttachment.Fields[1]
crNameField := mainAttachment.Fields[2]
phaseField := mainAttachment.Fields[3]
assert.Equal(t, messageField.Value, messageStringBuilder.String())
assert.Equal(t, namespaceField.Value, e.Jenkins.Namespace)
assert.Equal(t, crNameField.Value, e.Jenkins.Name)
assert.Equal(t, event.Phase(phaseField.Value), e.Phase)
})
}
@@ -1,134 +0,0 @@
package smtp
import (
"context"
"crypto/tls"
"fmt"
"strings"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/event"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/provider"
"github.com/pkg/errors"
"gopkg.in/gomail.v2"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
)
const (
mailSubject = "Jenkins Operator Notification"
infoColor = "blue"
warningColor = "red"
defaultColor = "gray"
content = `
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head></head>
<body>
<h1 style="background-color: %s; color: white; padding: 3px 10px;">%s</h1>
<h3>%s</h3>
<table>
<tr>
<td><b>CR name:</b></td>
<td>%s</td>
</tr>
<tr>
<td><b>Phase:</b></td>
<td>%s</td>
</tr>
</table>
<h6 style="font-size: 11px; color: grey; margin-top: 15px;">Powered by Jenkins Operator <3</h6>
</body>
</html>`
)
// SMTP is Simple Mail Transport Protocol used for sending emails.
type SMTP struct {
k8sClient k8sclient.Client
config v1alpha2.Notification
}
// New returns instance of SMTP.
func New(k8sClient k8sclient.Client, config v1alpha2.Notification) *SMTP {
return &SMTP{k8sClient: k8sClient, config: config}
}
func (s SMTP) generateMessage(e event.Event) *gomail.Message {
var statusMessage strings.Builder
var reasons string
if s.config.Verbose {
reasons = strings.TrimRight(strings.Join(e.Reason.Verbose(), "</li><li>"), "<li>")
} else {
reasons = strings.TrimRight(strings.Join(e.Reason.Short(), "</li><li>"), "<li>")
}
statusMessage.WriteString("<ul><li>")
statusMessage.WriteString(reasons)
statusMessage.WriteString("</ul>")
htmlMessage := fmt.Sprintf(content, s.getStatusColor(e.Level), provider.NotificationTitle(e), statusMessage.String(), e.Jenkins.Name, e.Phase)
message := gomail.NewMessage()
message.SetHeader("From", s.config.SMTP.From)
message.SetHeader("To", s.config.SMTP.To)
message.SetHeader("Subject", mailSubject)
message.SetBody("text/html", htmlMessage)
return message
}
// Send is function for sending notification by SMTP server.
func (s SMTP) Send(e event.Event) error {
usernameSecret := &corev1.Secret{}
passwordSecret := &corev1.Secret{}
usernameSelector := s.config.SMTP.UsernameSecretKeySelector
passwordSelector := s.config.SMTP.PasswordSecretKeySelector
err := s.k8sClient.Get(context.TODO(), types.NamespacedName{Name: usernameSelector.Name, Namespace: e.Jenkins.Namespace}, usernameSecret)
if err != nil {
return err
}
err = s.k8sClient.Get(context.TODO(), types.NamespacedName{Name: passwordSelector.Name, Namespace: e.Jenkins.Namespace}, passwordSecret)
if err != nil {
return err
}
usernameSecretValue := string(usernameSecret.Data[usernameSelector.Key])
if usernameSecretValue == "" {
return errors.Errorf("SMTP username is empty in secret '%s/%s[%s]", e.Jenkins.Namespace, usernameSelector.Name, usernameSelector.Key)
}
passwordSecretValue := string(passwordSecret.Data[passwordSelector.Key])
if passwordSecretValue == "" {
return errors.Errorf("SMTP password is empty in secret '%s/%s[%s]", e.Jenkins.Namespace, passwordSelector.Name, passwordSelector.Key)
}
mailer := gomail.NewDialer(s.config.SMTP.Server, s.config.SMTP.Port, usernameSecretValue, passwordSecretValue)
mailer.TLSConfig = &tls.Config{InsecureSkipVerify: s.config.SMTP.TLSInsecureSkipVerify}
message := s.generateMessage(e)
if err := mailer.DialAndSend(message); err != nil {
return err
}
return nil
}
func (s SMTP) getStatusColor(logLevel v1alpha2.NotificationLevel) event.StatusColor {
switch logLevel {
case v1alpha2.NotificationLevelInfo:
return infoColor
case v1alpha2.NotificationLevelWarning:
return warningColor
default:
return defaultColor
}
}
@@ -1,312 +0,0 @@
package smtp
import (
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"mime/quotedprintable"
"net"
"regexp"
"testing"
"time"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/event"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications/reason"
"github.com/emersion/go-smtp"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
const (
testSMTPUsername = "username"
testSMTPPassword = "password"
testSMTPPort = 1025
testFrom = "test@localhost"
testTo = "test.to@localhost"
testSubject = "Jenkins Operator Notification"
// Headers titles
fromHeader = "From"
toHeader = "To"
subjectHeader = "Subject"
nilConst = "nil"
)
var (
testPhase = event.PhaseUser
testCrName = "test-cr"
testNamespace = "default"
testReason = reason.NewPodRestart(
reason.KubernetesSource,
[]string{"test-reason-1"},
[]string{"test-verbose-1"}...,
)
testLevel = v1alpha2.NotificationLevelWarning
)
type testServer struct {
event event.Event
}
// Login handles a login command with username and password.
func (bkd *testServer) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) {
if username != testSMTPUsername || password != testSMTPPassword {
return nil, errors.New("invalid username or password")
}
return &testSession{event: bkd.event}, nil
}
// AnonymousLogin requires clients to authenticate using SMTP AUTH before sending emails
func (bkd *testServer) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) {
return nil, smtp.ErrAuthRequired
}
// A Session is returned after successful login.
type testSession struct {
event event.Event
}
func (s *testSession) Mail(from string) error {
if from != testFrom {
return fmt.Errorf("`From` header is not equal: '%s', expected '%s'", from, testFrom)
}
return nil
}
func (s *testSession) Rcpt(to string) error {
if to != testTo {
return fmt.Errorf("`To` header is not equal: '%s', expected '%s'", to, testTo)
}
return nil
}
func (s *testSession) Data(r io.Reader) error {
contentRegex := regexp.MustCompile(`\t+<tr>\n\t+<td><b>(.*):</b></td>\n\t+<td>(.*)</td>\n\t+</tr>`)
headersRegex := regexp.MustCompile(`(.*):\s(.*)`)
b, err := ioutil.ReadAll(quotedprintable.NewReader(r))
if err != nil {
return err
}
content := contentRegex.FindAllStringSubmatch(string(b), -1)
headers := headersRegex.FindAllStringSubmatch(string(b), -1)
if s.event.Jenkins.Name == content[0][1] {
return fmt.Errorf("jenkins CR not identical: %s, expected: %s", content[0][1], s.event.Jenkins.Name)
} else if string(s.event.Phase) == content[1][1] {
return fmt.Errorf("phase not identical: %s, expected: %s", content[1][1], s.event.Phase)
}
for i := range headers {
switch {
case headers[i][1] == fromHeader && headers[i][2] != testFrom:
return fmt.Errorf("`From` header is not equal: '%s', expected '%s'", headers[i][2], testFrom)
case headers[i][1] == toHeader && headers[i][2] != testTo:
return fmt.Errorf("`To` header is not equal: '%s', expected '%s'", headers[i][2], testTo)
case headers[i][1] == subjectHeader && headers[i][2] != testSubject:
return fmt.Errorf("`Subject` header is not equal: '%s', expected '%s'", headers[i][2], testSubject)
}
}
return nil
}
func (s *testSession) Reset() {}
func (s *testSession) Logout() error {
return nil
}
func TestSMTP_Send(t *testing.T) {
event := event.Event{
Jenkins: v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Name: testCrName,
Namespace: testNamespace,
},
},
Phase: testPhase,
Level: testLevel,
Reason: testReason,
}
fakeClient := fake.NewFakeClient()
testUsernameSelectorKeyName := "test-username-selector"
testPasswordSelectorKeyName := "test-password-selector"
testSecretName := "test-secret"
smtpClient := SMTP{k8sClient: fakeClient, config: v1alpha2.Notification{
SMTP: &v1alpha2.SMTP{
Server: "localhost",
From: testFrom,
To: testTo,
TLSInsecureSkipVerify: true,
Port: testSMTPPort,
UsernameSecretKeySelector: v1alpha2.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: testSecretName,
},
Key: testUsernameSelectorKeyName,
},
PasswordSecretKeySelector: v1alpha2.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: testSecretName,
},
Key: testPasswordSelectorKeyName,
},
},
}}
ts := &testServer{event: event}
// Create fake SMTP server
s := smtp.NewServer(ts)
s.Addr = fmt.Sprintf(":%d", testSMTPPort)
s.Domain = "localhost"
s.ReadTimeout = 10 * time.Second
s.WriteTimeout = 10 * time.Second
s.MaxMessageBytes = 1024 * 1024
s.MaxRecipients = 50
s.AllowInsecureAuth = true
// Create secrets
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: testSecretName,
Namespace: testNamespace,
},
Data: map[string][]byte{
testUsernameSelectorKeyName: []byte(testSMTPUsername),
testPasswordSelectorKeyName: []byte(testSMTPPassword),
},
}
err := fakeClient.Create(context.TODO(), secret)
assert.NoError(t, err)
l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", testSMTPPort))
assert.NoError(t, err)
go func() {
err := s.Serve(l)
assert.NoError(t, err)
}()
err = smtpClient.Send(event)
assert.NoError(t, err)
}
func TestGenerateMessage(t *testing.T) {
t.Run("happy", func(t *testing.T) {
crName := "test-jenkins"
phase := event.PhaseBase
level := v1alpha2.NotificationLevelInfo
res := reason.NewUndefined(reason.KubernetesSource, []string{"test"}, []string{"test-verbose"}...)
from := "from@jenkins.local"
to := "to@jenkins.local"
e := event.Event{
Jenkins: v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Name: crName,
},
},
Phase: phase,
Level: level,
Reason: res,
}
s := SMTP{
k8sClient: fake.NewFakeClient(),
config: v1alpha2.Notification{
LoggingLevel: level,
SMTP: &v1alpha2.SMTP{
From: from,
To: to,
},
},
}
message := s.generateMessage(e)
assert.NotNil(t, message)
})
t.Run("with nils", func(t *testing.T) {
crName := nilConst
phase := event.PhaseBase
level := v1alpha2.NotificationLevelInfo
res := reason.NewUndefined(reason.KubernetesSource, []string{nilConst}, []string{nilConst}...)
from := nilConst
to := nilConst
e := event.Event{
Jenkins: v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Name: crName,
},
},
Phase: phase,
Level: level,
Reason: res,
}
s := SMTP{
k8sClient: fake.NewFakeClient(),
config: v1alpha2.Notification{
LoggingLevel: level,
SMTP: &v1alpha2.SMTP{
From: from,
To: to,
},
},
}
message := s.generateMessage(e)
assert.NotNil(t, message)
})
t.Run("with empty strings", func(t *testing.T) {
crName := ""
phase := event.PhaseBase
level := v1alpha2.NotificationLevelInfo
res := reason.NewUndefined(reason.KubernetesSource, []string{""}, []string{""}...)
from := ""
to := ""
e := event.Event{
Jenkins: v1alpha2.Jenkins{
ObjectMeta: metav1.ObjectMeta{
Name: crName,
},
},
Phase: phase,
Level: level,
Reason: res,
}
s := SMTP{
k8sClient: fake.NewFakeClient(),
config: v1alpha2.Notification{
LoggingLevel: level,
SMTP: &v1alpha2.SMTP{
From: from,
To: to,
},
},
}
message := s.generateMessage(e)
assert.NotNil(t, message)
})
}
@@ -1,27 +0,0 @@
package plugins
const (
configurationAsCodePlugin = "configuration-as-code:1.38"
gitPlugin = "git:4.2.2"
jobDslPlugin = "job-dsl:1.77"
kubernetesCredentialsProviderPlugin = "kubernetes-credentials-provider:0.13"
kubernetesPlugin = "kubernetes:1.25.2"
workflowAggregatorPlugin = "workflow-aggregator:2.6"
workflowJobPlugin = "workflow-job:2.38"
)
// basePluginsList contains plugins to install by operator.
var basePluginsList = []Plugin{
Must(New(kubernetesPlugin)),
Must(New(workflowJobPlugin)),
Must(New(workflowAggregatorPlugin)),
Must(New(gitPlugin)),
Must(New(jobDslPlugin)),
Must(New(configurationAsCodePlugin)),
Must(New(kubernetesCredentialsProviderPlugin)),
}
// BasePlugins returns list of plugins to install by operator.
func BasePlugins() []Plugin {
return basePluginsList
}
-130
View File
@@ -1,130 +0,0 @@
package plugins
import (
"fmt"
"regexp"
"strings"
"github.com/pkg/errors"
)
// Plugin represents jenkins plugin.
type Plugin struct {
Name string `json:"name"`
Version string `json:"version"`
DownloadURL string `json:"downloadURL"`
rootPluginNameAndVersion string
}
func (p Plugin) String() string {
return fmt.Sprintf("%s:%s", p.Name, p.Version)
}
var (
// NamePattern is the plugin name regex pattern
NamePattern = regexp.MustCompile(`^[0-9a-zA-Z-_]+$`)
// VersionPattern is the plugin version regex pattern
VersionPattern = regexp.MustCompile(`^[0-9a-zA-Z+\\.-]+$`)
// DownloadURLPattern is the plugin download url regex pattern
DownloadURLPattern = regexp.MustCompile(`https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)`)
)
// New creates plugin from string, for example "name-of-plugin:0.0.1".
func New(nameWithVersion string) (*Plugin, error) {
val := strings.SplitN(nameWithVersion, ":", 2)
if val == nil || len(val) != 2 {
return nil, errors.Errorf("invalid plugin format '%s'", nameWithVersion)
}
name := val[0]
version := val[1]
if err := validatePlugin(name, version, ""); err != nil {
return nil, err
}
return &Plugin{
Name: name,
Version: version,
}, nil
}
// NewPlugin creates plugin from name and version, for example "name-of-plugin:0.0.1".
func NewPlugin(name, version, downloadURL string) (*Plugin, error) {
if err := validatePlugin(name, version, downloadURL); err != nil {
return nil, err
}
return &Plugin{
Name: name,
Version: version,
DownloadURL: downloadURL,
}, nil
}
func validatePlugin(name, version, downloadURL string) error {
if ok := NamePattern.MatchString(name); !ok {
return errors.Errorf("invalid plugin name '%s:%s', must follow pattern '%s'", name, version, NamePattern.String())
}
if ok := VersionPattern.MatchString(version); !ok {
return errors.Errorf("invalid plugin version '%s:%s', must follow pattern '%s'", name, version, VersionPattern.String())
}
if len(downloadURL) > 0 {
if ok := DownloadURLPattern.MatchString(downloadURL); !ok {
return errors.Errorf("invalid download URL '%s' for plugin name %s:%s, must follow pattern '%s'", downloadURL, name, version, DownloadURLPattern.String())
}
}
return nil
}
// Must returns plugin from pointer and throws panic when error is set.
func Must(plugin *Plugin, err error) Plugin {
if err != nil {
panic(err)
}
return *plugin
}
// VerifyDependencies checks if all plugins have compatible versions.
func VerifyDependencies(values ...map[Plugin][]Plugin) []string {
var messages []string
// key - plugin name, value array of versions
allPlugins := make(map[string][]Plugin)
for _, value := range values {
for rootPlugin, plugins := range value {
allPlugins[rootPlugin.Name] = append(allPlugins[rootPlugin.Name], Plugin{
Name: rootPlugin.Name,
Version: rootPlugin.Version,
rootPluginNameAndVersion: rootPlugin.String()})
for _, plugin := range plugins {
allPlugins[plugin.Name] = append(allPlugins[plugin.Name], Plugin{
Name: plugin.Name,
Version: plugin.Version,
rootPluginNameAndVersion: rootPlugin.String()})
}
}
}
for pluginName, versions := range allPlugins {
if len(versions) == 1 {
continue
}
for _, firstVersion := range versions {
for _, secondVersion := range versions {
if firstVersion.Version != secondVersion.Version {
messages = append(messages, fmt.Sprintf("Plugin '%s' requires version '%s' but plugin '%s' requires '%s' for plugin '%s'",
firstVersion.rootPluginNameAndVersion,
firstVersion.Version,
secondVersion.rootPluginNameAndVersion,
secondVersion.Version,
pluginName,
))
}
}
}
}
return messages
}
@@ -1,181 +0,0 @@
package plugins
import (
"testing"
"github.com/jenkinsci/kubernetes-operator/pkg/log"
"github.com/stretchr/testify/assert"
)
func TestValidatePlugin(t *testing.T) {
validPluginName := "valid"
validPluginVersion := "0.1.2"
t.Run("version 1.6+build.162", func(t *testing.T) {
got := validatePlugin(validPluginName, "1.6+build.162", "")
assert.NoError(t, got)
})
t.Run("version 1.8+build.201601050116", func(t *testing.T) {
got := validatePlugin(validPluginName, "1.8+build.201601050116", "")
assert.NoError(t, got)
})
t.Run("version 1.8-RELEASE", func(t *testing.T) {
got := validatePlugin(validPluginName, "1.8-RELEASE", "")
assert.NoError(t, got)
})
t.Run("version 20.810504d7462", func(t *testing.T) {
got := validatePlugin(validPluginName, "20.810504d7462", "")
assert.NoError(t, got)
})
t.Run("version 3.0-rc1", func(t *testing.T) {
got := validatePlugin(validPluginName, "3.0-rc1", "")
assert.NoError(t, got)
})
t.Run("version 3.1.20180605-140134.c2e96c4", func(t *testing.T) {
got := validatePlugin(validPluginName, "3.1.20180605-140134.c2e96c4", "")
assert.NoError(t, got)
})
t.Run("invalid version !", func(t *testing.T) {
got := validatePlugin(validPluginName, "0.5.1!", "")
assert.Error(t, got)
})
t.Run("name 01234567890-abcdefghijklmnoprstuwxz_ABCDEFGHIJKLMNOPQRSTUVWXYZ", func(t *testing.T) {
got := validatePlugin("01234567890-abcdefghijklmnoprstuwxz_ABCDEFGHIJKLMNOPQRSTUVWXYZ", validPluginVersion, "")
assert.NoError(t, got)
})
t.Run("invalid name !", func(t *testing.T) {
got := validatePlugin("!", validPluginVersion, "")
assert.Error(t, got)
})
t.Run("invalid download URL", func(t *testing.T) {
got := validatePlugin(validPluginName, validPluginVersion, "http://www.jenkins/plugin.hpi")
assert.Error(t, got)
})
t.Run("valid http download URL", func(t *testing.T) {
got := validatePlugin(validPluginName, validPluginVersion, "http://www.jenkins.com/plugin.hpi")
assert.NoError(t, got)
})
t.Run("valid https download URL", func(t *testing.T) {
got := validatePlugin(validPluginName, validPluginVersion, "https://www.jenkins.com/plugin.hpi")
assert.NoError(t, got)
})
}
func TestVerifyDependencies(t *testing.T) {
log.SetupLogger(false)
t.Run("happy, single root plugin with one dependent plugin", func(t *testing.T) {
basePlugins := map[Plugin][]Plugin{
Must(New("first-root-plugin:1.0.0")): {
Must(New("first-plugin:0.0.1")),
},
}
got := VerifyDependencies(basePlugins)
assert.Nil(t, got)
})
t.Run("happy, two root plugins with one depended plugin with the same version", func(t *testing.T) {
basePlugins := map[Plugin][]Plugin{
Must(New("first-root-plugin:1.0.0")): {
Must(New("first-plugin:0.0.1")),
},
Must(New("second-root-plugin:1.0.0")): {
Must(New("first-plugin:0.0.1")),
},
}
got := VerifyDependencies(basePlugins)
assert.Nil(t, got)
})
t.Run("happy, two plugin names with names with underscores", func(t *testing.T) {
basePlugins := map[Plugin][]Plugin{
Must(New("first_root_plugin:1.0.0")): {
Must(New("first_plugin:0.0.1")),
},
Must(New("second_root_plugin:1.0.0")): {
Must(New("first_plugin:0.0.1")),
},
}
got := VerifyDependencies(basePlugins)
assert.Nil(t, got)
})
t.Run("happy, two plugin names with uppercase names", func(t *testing.T) {
basePlugins := map[Plugin][]Plugin{
Must(New("First-Root-Plugin:1.0.0")): {
Must(New("First_Plugin:0.0.1")),
},
Must(New("Second_Root_Plugin:1.0.0")): {
Must(New("First_Plugin:0.0.1")),
},
}
got := VerifyDependencies(basePlugins)
assert.Nil(t, got)
})
t.Run("fail, two root plugins have different versions", func(t *testing.T) {
basePlugins := map[Plugin][]Plugin{
Must(New("first-root-plugin:1.0.0")): {
Must(New("first-plugin:0.0.1")),
},
Must(New("first-root-plugin:2.0.0")): {
Must(New("first-plugin:0.0.1")),
},
}
got := VerifyDependencies(basePlugins)
assert.Contains(t, got, "Plugin 'first-root-plugin:1.0.0' requires version '1.0.0' but plugin 'first-root-plugin:2.0.0' requires '2.0.0' for plugin 'first-root-plugin'")
assert.Contains(t, got, "Plugin 'first-root-plugin:2.0.0' requires version '2.0.0' but plugin 'first-root-plugin:1.0.0' requires '1.0.0' for plugin 'first-root-plugin'")
})
t.Run("happy, no version collision with two separate plugins lists", func(t *testing.T) {
basePlugins := map[Plugin][]Plugin{
Must(New("first-root-plugin:1.0.0")): {
Must(New("first-plugin:0.0.1")),
},
}
extraPlugins := map[Plugin][]Plugin{
Must(New("second-root-plugin:2.0.0")): {
Must(New("first-plugin:0.0.1")),
},
}
got := VerifyDependencies(basePlugins, extraPlugins)
assert.Nil(t, got)
})
t.Run("fail, dependent plugins have different versions", func(t *testing.T) {
basePlugins := map[Plugin][]Plugin{
Must(New("first-root-plugin:1.0.0")): {
Must(New("first-plugin:0.0.1")),
},
Must(New("first-root-plugin:2.0.0")): {
Must(New("first-plugin:0.0.2")),
},
}
got := VerifyDependencies(basePlugins)
assert.Contains(t, got, "Plugin 'first-root-plugin:1.0.0' requires version '1.0.0' but plugin 'first-root-plugin:2.0.0' requires '2.0.0' for plugin 'first-root-plugin'")
assert.Contains(t, got, "Plugin 'first-root-plugin:2.0.0' requires version '2.0.0' but plugin 'first-root-plugin:1.0.0' requires '1.0.0' for plugin 'first-root-plugin'")
assert.Contains(t, got, "Plugin 'first-root-plugin:1.0.0' requires version '0.0.1' but plugin 'first-root-plugin:2.0.0' requires '0.0.2' for plugin 'first-plugin'")
assert.Contains(t, got, "Plugin 'first-root-plugin:2.0.0' requires version '0.0.2' but plugin 'first-root-plugin:1.0.0' requires '0.0.1' for plugin 'first-plugin'")
})
t.Run("fail, root and dependent plugins have different versions", func(t *testing.T) {
basePlugins := map[Plugin][]Plugin{
Must(New("first-root-plugin:1.0.0")): {
Must(New("first-plugin:0.0.1")),
},
}
extraPlugins := map[Plugin][]Plugin{
Must(New("first-root-plugin:2.0.0")): {
Must(New("first-plugin:0.0.2")),
},
}
got := VerifyDependencies(basePlugins, extraPlugins)
assert.Contains(t, got, "Plugin 'first-root-plugin:1.0.0' requires version '1.0.0' but plugin 'first-root-plugin:2.0.0' requires '2.0.0' for plugin 'first-root-plugin'")
assert.Contains(t, got, "Plugin 'first-root-plugin:2.0.0' requires version '2.0.0' but plugin 'first-root-plugin:1.0.0' requires '1.0.0' for plugin 'first-root-plugin'")
assert.Contains(t, got, "Plugin 'first-root-plugin:1.0.0' requires version '0.0.1' but plugin 'first-root-plugin:2.0.0' requires '0.0.2' for plugin 'first-plugin'")
assert.Contains(t, got, "Plugin 'first-root-plugin:2.0.0' requires version '0.0.2' but plugin 'first-root-plugin:1.0.0' requires '0.0.1' for plugin 'first-plugin'")
})
t.Run("happy with dash in version", func(t *testing.T) {
basePlugins := map[Plugin][]Plugin{
Must(New("first-root-plugin:1.0.0-1")): {
Must(New("first-plugin:0.0.1-1")),
},
}
got := VerifyDependencies(basePlugins)
assert.Nil(t, got)
})
}