mirror of
https://github.com/jenkinsci/kubernetes-operator.git
synced 2026-10-20 11:20:08 +02:00
reorganise directories for better readbility
This commit is contained in:
@@ -1,2 +0,0 @@
|
||||
// Package client contains client for Jenkins API
|
||||
package client
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -1,2 +0,0 @@
|
||||
// Package groovy implements groovy scripts execution via Jenkins Job
|
||||
package groovy
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user