Merge branch 'master' into fix-plugin-version

This commit is contained in:
Jakub Al-Khalili 2019-10-09 13:00:39 +02:00 committed by GitHub
commit 26cf1242ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 701 additions and 547 deletions

View File

@ -1,5 +1,4 @@
language: go language: go
sudo: required
env: env:
global: global:
@ -11,7 +10,7 @@ env:
- KUBECONFIG=$HOME/.kube/config - KUBECONFIG=$HOME/.kube/config
go: go:
- 1.12.x - 1.13.x
matrix: matrix:
fast_finish: true fast_finish: true

View File

@ -200,10 +200,10 @@ PLATFORM = $(shell echo $(UNAME_S) | tr A-Z a-z)
staticcheck: ## Verifies `staticcheck` passes staticcheck: ## Verifies `staticcheck` passes
@echo "+ $@" @echo "+ $@"
ifndef HAS_STATICCHECK ifndef HAS_STATICCHECK
wget https://github.com/dominikh/go-tools/releases/download/2019.1.1/staticcheck_$(PLATFORM)_amd64 wget -O staticcheck_$(PLATFORM)_amd64.tar.gz https://github.com/dominikh/go-tools/releases/download/2019.2.3/staticcheck_$(PLATFORM)_amd64.tar.gz
chmod +x staticcheck_$(PLATFORM)_amd64 tar zxvf staticcheck_$(PLATFORM)_amd64.tar.gz
mkdir -p $(GOPATH)/bin mkdir -p $(GOPATH)/bin
mv staticcheck_$(PLATFORM)_amd64 $(GOPATH)/bin/staticcheck mv staticcheck/staticcheck $(GOPATH)/bin
endif endif
@staticcheck $(PACKAGES) @staticcheck $(PACKAGES)

View File

@ -13,6 +13,7 @@ import (
"github.com/jenkinsci/kubernetes-operator/pkg/apis" "github.com/jenkinsci/kubernetes-operator/pkg/apis"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications"
"github.com/jenkinsci/kubernetes-operator/pkg/event" "github.com/jenkinsci/kubernetes-operator/pkg/event"
"github.com/jenkinsci/kubernetes-operator/pkg/log" "github.com/jenkinsci/kubernetes-operator/pkg/log"
"github.com/jenkinsci/kubernetes-operator/version" "github.com/jenkinsci/kubernetes-operator/version"
@ -118,8 +119,11 @@ func main() {
fatal(errors.Wrap(err, "failed to create Kubernetes client set"), *debug) fatal(errors.Wrap(err, "failed to create Kubernetes client set"), *debug)
} }
c := make(chan notifications.Event)
go notifications.Listen(c, events, mgr.GetClient())
// setup Jenkins controller // setup Jenkins controller
if err := jenkins.Add(mgr, *local, *minikube, events, *clientSet, *cfg); err != nil { if err := jenkins.Add(mgr, *local, *minikube, *clientSet, *cfg, &c); err != nil {
fatal(errors.Wrap(err, "failed to setup controllers"), *debug) fatal(errors.Wrap(err, "failed to setup controllers"), *debug)
} }

View File

@ -1,15 +1,15 @@
# Setup variables for the Makefile # Setup variables for the Makefile
NAME=kubernetes-operator NAME=kubernetes-operator
OPERATOR_SDK_VERSION=0.10.0 OPERATOR_SDK_VERSION=0.10.0
GO_VERSION=1.12.6 GO_VERSION=1.13.1
PKG=github.com/jenkinsci/kubernetes-operator PKG=github.com/jenkinsci/kubernetes-operator
DOCKER_ORGANIZATION=virtuslab DOCKER_ORGANIZATION=virtuslab
DOCKER_REGISTRY=jenkins-operator DOCKER_REGISTRY=jenkins-operator
NAMESPACE=default NAMESPACE=default
API_VERSION=v1alpha2 API_VERSION=v1alpha2
MINIKUBE_KUBERNETES_VERSION=v1.12.9 MINIKUBE_KUBERNETES_VERSION=v1.16.0
MINIKUBE_DRIVER=virtualbox MINIKUBE_DRIVER=virtualbox
MINIKUBE_VERSION=1.2.0 MINIKUBE_VERSION=1.4.0
KUBECTL_CONTEXT=minikube KUBECTL_CONTEXT=minikube
ALL_IN_ONE_DEPLOY_FILE_PREFIX=all-in-one ALL_IN_ONE_DEPLOY_FILE_PREFIX=all-in-one
GEN_CRD_API=gen-crd-api-reference-docs GEN_CRD_API=gen-crd-api-reference-docs

View File

@ -142,7 +142,7 @@ pipelineJob('build-jenkins-operator') {
} }
``` ```
**cicd/jobs/build.jenkins** it's an actual Jenkins pipeline: **cicd/pipelines/build.jenkins** it's an actual Jenkins pipeline:
``` ```
#!/usr/bin/env groovy #!/usr/bin/env groovy
@ -418,7 +418,7 @@ apiVersion: v1
kind: PersistentVolumeClaim kind: PersistentVolumeClaim
metadata: metadata:
name: <pvc_name> name: <pvc_name>
namespace: <namesapce> namespace: <namespace>
spec: spec:
accessModes: accessModes:
- ReadWriteOnce - ReadWriteOnce
@ -429,7 +429,7 @@ spec:
Run command: Run command:
```bash ```bash
$ kubectl -n <namesapce> create -f pvc.yaml $ kubectl -n <namespace> create -f pvc.yaml
``` ```
#### Configure Jenkins CR #### Configure Jenkins CR

View File

@ -144,7 +144,7 @@ pipelineJob('build-jenkins-operator') {
} }
``` ```
**cicd/jobs/build.jenkins** it's an actual Jenkins pipeline: **cicd/pipelines/build.jenkins** it's an actual Jenkins pipeline:
``` ```
#!/usr/bin/env groovy #!/usr/bin/env groovy
@ -563,7 +563,7 @@ apiVersion: v1
kind: PersistentVolumeClaim kind: PersistentVolumeClaim
metadata: metadata:
name: <pvc_name> name: <pvc_name>
namespace: <namesapce> namespace: <namespace>
spec: spec:
accessModes: accessModes:
- ReadWriteOnce - ReadWriteOnce
@ -574,7 +574,7 @@ spec:
Run command: Run command:
```bash ```bash
$ kubectl -n <namesapce> create -f pvc.yaml $ kubectl -n <namespace> create -f pvc.yaml
``` ```
#### Configure Jenkins CR #### Configure Jenkins CR

8
go.mod
View File

@ -1,5 +1,7 @@
module github.com/jenkinsci/kubernetes-operator module github.com/jenkinsci/kubernetes-operator
go 1.13
require ( require (
github.com/bndr/gojenkins v0.0.0-20181125150310-de43c03cf849 github.com/bndr/gojenkins v0.0.0-20181125150310-de43c03cf849
github.com/docker/distribution v2.7.1+incompatible github.com/docker/distribution v2.7.1+incompatible
@ -17,11 +19,11 @@ require (
github.com/stretchr/testify v1.3.0 github.com/stretchr/testify v1.3.0
go.uber.org/zap v1.9.1 go.uber.org/zap v1.9.1
golang.org/x/crypto v0.0.0-20190909091759-094676da4a83 // indirect golang.org/x/crypto v0.0.0-20190909091759-094676da4a83 // indirect
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac // indirect golang.org/x/lint v0.0.0-20190930215403-16217165b5de // indirect
golang.org/x/net v0.0.0-20190909003024-a7b16738d86b // indirect golang.org/x/net v0.0.0-20190909003024-a7b16738d86b // indirect
golang.org/x/sys v0.0.0-20190910064555-bbd175535a8b // indirect golang.org/x/sys v0.0.0-20190910064555-bbd175535a8b // indirect
golang.org/x/text v0.3.2 // indirect golang.org/x/text v0.3.2 // indirect
golang.org/x/tools v0.0.0-20190910044552-dd2b5c81c578 // indirect golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3 // indirect
k8s.io/api v0.0.0-20190612125737-db0771252981 k8s.io/api v0.0.0-20190612125737-db0771252981
k8s.io/apimachinery v0.0.0-20190612125636-6a5db36e93ad k8s.io/apimachinery v0.0.0-20190612125636-6a5db36e93ad
k8s.io/client-go v11.0.0+incompatible k8s.io/client-go v11.0.0+incompatible
@ -48,5 +50,3 @@ replace (
sigs.k8s.io/controller-runtime => sigs.k8s.io/controller-runtime v0.1.10 sigs.k8s.io/controller-runtime => sigs.k8s.io/controller-runtime v0.1.10
sigs.k8s.io/controller-tools => sigs.k8s.io/controller-tools v0.1.11-0.20190411181648-9d55346c2bde sigs.k8s.io/controller-tools => sigs.k8s.io/controller-tools v0.1.11-0.20190411181648-9d55346c2bde
) )
replace git.apache.org/thrift.git => github.com/apache/thrift v0.12.0

7
go.sum
View File

@ -10,6 +10,7 @@ contrib.go.opencensus.io/exporter/ocagent v0.4.9 h1:8ZbMXpyd04/3LILa/9Tzr8N4HzZN
contrib.go.opencensus.io/exporter/ocagent v0.4.9/go.mod h1:ueLzZcP7LPhPulEBukGn4aLh7Mx9YJwpVJ9nL2FYltw= contrib.go.opencensus.io/exporter/ocagent v0.4.9/go.mod h1:ueLzZcP7LPhPulEBukGn4aLh7Mx9YJwpVJ9nL2FYltw=
contrib.go.opencensus.io/exporter/ocagent v0.4.11 h1:Zwy9skaqR2igcEfSVYDuAsbpa33N0RPtnYTHEe2whPI= contrib.go.opencensus.io/exporter/ocagent v0.4.11 h1:Zwy9skaqR2igcEfSVYDuAsbpa33N0RPtnYTHEe2whPI=
contrib.go.opencensus.io/exporter/ocagent v0.4.11/go.mod h1:7ihiYRbdcVfW4m4wlXi9WRPdv79C0fStcjNlyE6ek9s= contrib.go.opencensus.io/exporter/ocagent v0.4.11/go.mod h1:7ihiYRbdcVfW4m4wlXi9WRPdv79C0fStcjNlyE6ek9s=
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
git.apache.org/thrift.git v0.12.0/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= git.apache.org/thrift.git v0.12.0/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Azure/go-autorest v11.1.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest v11.1.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
@ -438,6 +439,8 @@ golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f h1:hX65Cu3JDlGH3uEdK7I99Ii+
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac h1:8R1esu+8QioDxo4E4mX6bFztO+dMTM49DNAaWfO5OeY= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac h1:8R1esu+8QioDxo4E4mX6bFztO+dMTM49DNAaWfO5OeY=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -526,6 +529,10 @@ golang.org/x/tools v0.0.0-20190708203411-c8855242db9c h1:rRFNgkkT7zOyWlroLBmsrKY
golang.org/x/tools v0.0.0-20190708203411-c8855242db9c/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= golang.org/x/tools v0.0.0-20190708203411-c8855242db9c/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
golang.org/x/tools v0.0.0-20190910044552-dd2b5c81c578 h1:f0Gfd654rnnfXT1+BK1YHPTS1qQdKrPIaGQwWxNE44k= golang.org/x/tools v0.0.0-20190910044552-dd2b5c81c578 h1:f0Gfd654rnnfXT1+BK1YHPTS1qQdKrPIaGQwWxNE44k=
golang.org/x/tools v0.0.0-20190910044552-dd2b5c81c578/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190910044552-dd2b5c81c578/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e h1:1xWUkZQQ9Z9UuZgNaIR6OQOE7rUFglXUUBZlO+dGg6I=
golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3 h1:2AmBLzhAfXj+2HCW09VCkJtHIYgHTIPcTeYqgP7Bwt0=
golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=

View File

@ -17,9 +17,9 @@ type JenkinsSpec struct {
// +optional // +optional
SeedJobs []SeedJob `json:"seedJobs,omitempty"` SeedJobs []SeedJob `json:"seedJobs,omitempty"`
/* // Notifications defines list of a services which are used to inform about Jenkins status // Notifications defines list of a services which are used to inform about Jenkins status
// Can be used to integrate chat services like Slack, Microsoft MicrosoftTeams or Mailgun // Can be used to integrate chat services like Slack, Microsoft Teams or Mailgun
Notifications []Notification `json:"notifications,omitempty"`*/ Notifications []Notification `json:"notifications,omitempty"`
// Service is Kubernetes service of Jenkins master HTTP pod // Service is Kubernetes service of Jenkins master HTTP pod
// Defaults to : // Defaults to :

View File

@ -342,6 +342,13 @@ func (in *JenkinsSpec) DeepCopyInto(out *JenkinsSpec) {
*out = make([]SeedJob, len(*in)) *out = make([]SeedJob, len(*in))
copy(*out, *in) copy(*out, *in)
} }
if in.Notifications != nil {
in, out := &in.Notifications, &out.Notifications
*out = make([]Notification, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
in.Service.DeepCopyInto(&out.Service) in.Service.DeepCopyInto(&out.Service)
in.SlaveService.DeepCopyInto(&out.SlaveService) in.SlaveService.DeepCopyInto(&out.SlaveService)
in.Backup.DeepCopyInto(&out.Backup) in.Backup.DeepCopyInto(&out.Backup)

View File

@ -11,7 +11,12 @@ import (
) )
// GroovyScriptExecutionFailed is custom error type which indicates passed groovy script is invalid // GroovyScriptExecutionFailed is custom error type which indicates passed groovy script is invalid
type GroovyScriptExecutionFailed struct{} type GroovyScriptExecutionFailed struct {
ConfigurationType string
Source string
Name string
Logs string
}
func (e GroovyScriptExecutionFailed) Error() string { func (e GroovyScriptExecutionFailed) Error() string {
return "script execution failed" return "script execution failed"

View File

@ -46,8 +46,8 @@ func New(k8sClient k8s.Client, clientSet kubernetes.Clientset,
} }
// Validate validates backup and restore configuration // Validate validates backup and restore configuration
func (bar *BackupAndRestore) Validate() bool { func (bar *BackupAndRestore) Validate() []string {
valid := true var messages []string
allContainers := map[string]v1alpha2.Container{} allContainers := map[string]v1alpha2.Container{}
for _, container := range bar.jenkins.Spec.Master.Containers { for _, container := range bar.jenkins.Spec.Master.Containers {
allContainers[container.Name] = container allContainers[container.Name] = container
@ -57,12 +57,10 @@ func (bar *BackupAndRestore) Validate() bool {
if len(restore.ContainerName) > 0 { if len(restore.ContainerName) > 0 {
_, found := allContainers[restore.ContainerName] _, found := allContainers[restore.ContainerName]
if !found { if !found {
valid = false messages = append(messages, fmt.Sprintf("restore container '%s' not found in CR spec.master.containers", restore.ContainerName))
bar.logger.V(log.VWarn).Info(fmt.Sprintf("restore container '%s' not found in CR spec.master.containers", restore.ContainerName))
} }
if restore.Action.Exec == nil { if restore.Action.Exec == nil {
valid = false messages = append(messages, fmt.Sprintf("spec.restore.action.exec is not configured"))
bar.logger.V(log.VWarn).Info(fmt.Sprintf("spec.restore.action.exec is not configured"))
} }
} }
@ -70,29 +68,24 @@ func (bar *BackupAndRestore) Validate() bool {
if len(backup.ContainerName) > 0 { if len(backup.ContainerName) > 0 {
_, found := allContainers[backup.ContainerName] _, found := allContainers[backup.ContainerName]
if !found { if !found {
valid = false messages = append(messages, fmt.Sprintf("backup container '%s' not found in CR spec.master.containers", backup.ContainerName))
bar.logger.V(log.VWarn).Info(fmt.Sprintf("backup container '%s' not found in CR spec.master.containers", backup.ContainerName))
} }
if backup.Action.Exec == nil { if backup.Action.Exec == nil {
valid = false messages = append(messages, fmt.Sprintf("spec.backup.action.exec is not configured"))
bar.logger.V(log.VWarn).Info(fmt.Sprintf("spec.backup.action.exec is not configured"))
} }
if backup.Interval == 0 { if backup.Interval == 0 {
valid = false messages = append(messages, fmt.Sprintf("spec.backup.interval is not configured"))
bar.logger.V(log.VWarn).Info(fmt.Sprintf("spec.backup.interval is not configured"))
} }
} }
if len(restore.ContainerName) > 0 && len(backup.ContainerName) == 0 { if len(restore.ContainerName) > 0 && len(backup.ContainerName) == 0 {
valid = false messages = append(messages, "spec.backup.containerName is not configured")
bar.logger.V(log.VWarn).Info("spec.backup.containerName is not configured")
} }
if len(backup.ContainerName) > 0 && len(restore.ContainerName) == 0 { if len(backup.ContainerName) > 0 && len(restore.ContainerName) == 0 {
valid = false messages = append(messages, "spec.restore.containerName is not configured")
bar.logger.V(log.VWarn).Info("spec.restore.containerName is not configured")
} }
return valid return messages
} }
// Restore performs Jenkins restore backup operation // Restore performs Jenkins restore backup operation

View File

@ -14,6 +14,7 @@ import (
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/backuprestore" "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/base/resources"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/groovy" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/groovy"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/plugins" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/plugins"
"github.com/jenkinsci/kubernetes-operator/pkg/log" "github.com/jenkinsci/kubernetes-operator/pkg/log"
"github.com/jenkinsci/kubernetes-operator/version" "github.com/jenkinsci/kubernetes-operator/version"
@ -46,11 +47,13 @@ type ReconcileJenkinsBaseConfiguration struct {
local, minikube bool local, minikube bool
clientSet *kubernetes.Clientset clientSet *kubernetes.Clientset
config *rest.Config config *rest.Config
notificationEvents *chan notifications.Event
} }
// New create structure which takes care of base configuration // New create structure which takes care of base configuration
func New(client client.Client, scheme *runtime.Scheme, logger logr.Logger, func New(client client.Client, scheme *runtime.Scheme, logger logr.Logger,
jenkins *v1alpha2.Jenkins, local, minikube bool, clientSet *kubernetes.Clientset, config *rest.Config) *ReconcileJenkinsBaseConfiguration { jenkins *v1alpha2.Jenkins, local, minikube bool, clientSet *kubernetes.Clientset, config *rest.Config,
notificationEvents *chan notifications.Event) *ReconcileJenkinsBaseConfiguration {
return &ReconcileJenkinsBaseConfiguration{ return &ReconcileJenkinsBaseConfiguration{
k8sClient: client, k8sClient: client,
scheme: scheme, scheme: scheme,
@ -60,6 +63,7 @@ func New(client client.Client, scheme *runtime.Scheme, logger logr.Logger,
minikube: minikube, minikube: minikube,
clientSet: clientSet, clientSet: clientSet,
config: config, config: config,
notificationEvents: notificationEvents,
} }
} }
@ -124,6 +128,7 @@ func (r *ReconcileJenkinsBaseConfiguration) Reconcile() (reconcile.Result, jenki
} }
result, err = r.ensureBaseConfiguration(jenkinsClient) result, err = r.ensureBaseConfiguration(jenkinsClient)
return result, jenkinsClient, err return result, jenkinsClient, err
} }
@ -434,6 +439,12 @@ func (r *ReconcileJenkinsBaseConfiguration) ensureJenkinsMasterPod(meta metav1.O
resources.JenkinsMasterContainerName, []string{"bash", "-c", fmt.Sprintf("%s/%s && <custom-command-here> && /sbin/tini -s -- /usr/local/bin/jenkins.sh", resources.JenkinsMasterContainerName, []string{"bash", "-c", fmt.Sprintf("%s/%s && <custom-command-here> && /sbin/tini -s -- /usr/local/bin/jenkins.sh",
resources.JenkinsScriptsVolumePath, resources.InitScriptName)})) resources.JenkinsScriptsVolumePath, resources.InitScriptName)}))
} }
*r.notificationEvents <- notifications.Event{
Jenkins: *r.jenkins,
Phase: notifications.PhaseBase,
LogLevel: v1alpha2.NotificationLogLevelInfo,
Message: "Creating a new Jenkins Master Pod",
}
r.logger.Info(fmt.Sprintf("Creating a new Jenkins Master Pod %s/%s", jenkinsMasterPod.Namespace, jenkinsMasterPod.Name)) r.logger.Info(fmt.Sprintf("Creating a new Jenkins Master Pod %s/%s", jenkinsMasterPod.Namespace, jenkinsMasterPod.Name))
err = r.createResource(jenkinsMasterPod) err = r.createResource(jenkinsMasterPod)
if err != nil { if err != nil {

View File

@ -233,7 +233,7 @@ func TestCompareVolumes(t *testing.T) {
Volumes: resources.GetJenkinsMasterPodBaseVolumes(jenkins), Volumes: resources.GetJenkinsMasterPodBaseVolumes(jenkins),
}, },
} }
reconciler := New(nil, nil, nil, jenkins, false, false, nil, nil) reconciler := New(nil, nil, nil, jenkins, false, false, nil, nil, nil)
got := reconciler.compareVolumes(pod) got := reconciler.compareVolumes(pod)
@ -257,7 +257,7 @@ func TestCompareVolumes(t *testing.T) {
Volumes: resources.GetJenkinsMasterPodBaseVolumes(jenkins), Volumes: resources.GetJenkinsMasterPodBaseVolumes(jenkins),
}, },
} }
reconciler := New(nil, nil, nil, jenkins, false, false, nil, nil) reconciler := New(nil, nil, nil, jenkins, false, false, nil, nil, nil)
got := reconciler.compareVolumes(pod) got := reconciler.compareVolumes(pod)
@ -281,7 +281,7 @@ func TestCompareVolumes(t *testing.T) {
Volumes: append(resources.GetJenkinsMasterPodBaseVolumes(jenkins), corev1.Volume{Name: "added"}), Volumes: append(resources.GetJenkinsMasterPodBaseVolumes(jenkins), corev1.Volume{Name: "added"}),
}, },
} }
reconciler := New(nil, nil, nil, jenkins, false, false, nil, nil) reconciler := New(nil, nil, nil, jenkins, false, false, nil, nil, nil)
got := reconciler.compareVolumes(pod) got := reconciler.compareVolumes(pod)

View File

@ -6,13 +6,11 @@ import (
"regexp" "regexp"
"strings" "strings"
docker "github.com/docker/distribution/reference"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" "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/configuration/base/resources"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/plugins" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/plugins"
"github.com/jenkinsci/kubernetes-operator/pkg/log"
docker "github.com/docker/distribution/reference"
stackerr "github.com/pkg/errors" stackerr "github.com/pkg/errors"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors"
@ -24,213 +22,209 @@ var (
) )
// Validate validates Jenkins CR Spec.master section // Validate validates Jenkins CR Spec.master section
func (r *ReconcileJenkinsBaseConfiguration) Validate(jenkins *v1alpha2.Jenkins) (bool, error) { func (r *ReconcileJenkinsBaseConfiguration) Validate(jenkins *v1alpha2.Jenkins) ([]string, error) {
if !r.validateReservedVolumes() { var messages []string
return false, nil
if msg := r.validateReservedVolumes(); len(msg) > 0 {
messages = append(messages, msg...)
} }
if valid, err := r.validateVolumes(); err != nil {
return false, err if msg, err := r.validateVolumes(); err != nil {
} else if !valid { return nil, err
return false, nil } else if len(msg) > 0 {
messages = append(messages, msg...)
} }
for _, container := range jenkins.Spec.Master.Containers { for _, container := range jenkins.Spec.Master.Containers {
if !r.validateContainer(container) { if msg := r.validateContainer(container); len(msg) > 0 {
return false, nil for _, m := range msg {
messages = append(messages, fmt.Sprintf("Container `%s` - %s", container.Name, m))
}
} }
} }
if !r.validatePlugins(plugins.BasePlugins(), jenkins.Spec.Master.BasePlugins, jenkins.Spec.Master.Plugins) { if msg := r.validatePlugins(plugins.BasePlugins(), jenkins.Spec.Master.BasePlugins, jenkins.Spec.Master.Plugins); len(msg) > 0 {
return false, nil messages = append(messages, msg...)
} }
if !r.validateJenkinsMasterPodEnvs() { if msg := r.validateJenkinsMasterPodEnvs(); len(msg) > 0 {
return false, nil messages = append(messages, msg...)
} }
if valid, err := r.validateCustomization(r.jenkins.Spec.GroovyScripts.Customization, "spec.groovyScripts"); err != nil { if msg, err := r.validateCustomization(r.jenkins.Spec.GroovyScripts.Customization, "spec.groovyScripts"); err != nil {
return false, err return nil, err
} else if !valid { } else if len(msg) > 0 {
return false, nil messages = append(messages, msg...)
} }
if valid, err := r.validateCustomization(r.jenkins.Spec.ConfigurationAsCode.Customization, "spec.configurationAsCode"); err != nil { if msg, err := r.validateCustomization(r.jenkins.Spec.ConfigurationAsCode.Customization, "spec.configurationAsCode"); err != nil {
return false, err return nil, err
} else if !valid { } else if len(msg) > 0 {
return false, nil messages = append(messages, msg...)
} }
return true, nil return messages, nil
} }
func (r *ReconcileJenkinsBaseConfiguration) validateImagePullSecrets() (bool, error) { func (r *ReconcileJenkinsBaseConfiguration) validateImagePullSecrets() ([]string, error) {
var messages []string
for _, sr := range r.jenkins.Spec.Master.ImagePullSecrets { for _, sr := range r.jenkins.Spec.Master.ImagePullSecrets {
valid, err := r.validateImagePullSecret(sr.Name) msg, err := r.validateImagePullSecret(sr.Name)
if err != nil { if err != nil {
return false, err return nil, err
} }
if !valid { if len(msg) > 0 {
return false, nil messages = append(messages, msg...)
} }
} }
return true, nil return messages, nil
} }
func (r *ReconcileJenkinsBaseConfiguration) validateImagePullSecret(secretName string) (bool, error) { func (r *ReconcileJenkinsBaseConfiguration) validateImagePullSecret(secretName string) ([]string, error) {
var messages []string
secret := &corev1.Secret{} secret := &corev1.Secret{}
err := r.k8sClient.Get(context.TODO(), types.NamespacedName{Name: secretName, Namespace: r.jenkins.ObjectMeta.Namespace}, secret) err := r.k8sClient.Get(context.TODO(), types.NamespacedName{Name: secretName, Namespace: r.jenkins.ObjectMeta.Namespace}, secret)
if err != nil && apierrors.IsNotFound(err) { if err != nil && apierrors.IsNotFound(err) {
r.logger.V(log.VWarn).Info(fmt.Sprintf("Secret %s not found defined in spec.master.imagePullSecrets", secretName)) messages = append(messages, fmt.Sprintf("Secret %s not found defined in spec.master.imagePullSecrets", secretName))
return false, nil
} else if err != nil && !apierrors.IsNotFound(err) { } else if err != nil && !apierrors.IsNotFound(err) {
return false, stackerr.WithStack(err) return nil, stackerr.WithStack(err)
} }
if secret.Data["docker-server"] == nil { if secret.Data["docker-server"] == nil {
r.logger.V(log.VWarn).Info(fmt.Sprintf("Secret '%s' defined in spec.master.imagePullSecrets doesn't have 'docker-server' key.", secretName)) messages = append(messages, fmt.Sprintf("Secret '%s' defined in spec.master.imagePullSecrets doesn't have 'docker-server' key.", secretName))
return false, nil
} }
if secret.Data["docker-username"] == nil { if secret.Data["docker-username"] == nil {
r.logger.V(log.VWarn).Info(fmt.Sprintf("Secret '%s' defined in spec.master.imagePullSecrets doesn't have 'docker-username' key.", secretName)) messages = append(messages, fmt.Sprintf("Secret '%s' defined in spec.master.imagePullSecrets doesn't have 'docker-username' key.", secretName))
return false, nil
} }
if secret.Data["docker-password"] == nil { if secret.Data["docker-password"] == nil {
r.logger.V(log.VWarn).Info(fmt.Sprintf("Secret '%s' defined in spec.master.imagePullSecrets doesn't have 'docker-password' key.", secretName)) messages = append(messages, fmt.Sprintf("Secret '%s' defined in spec.master.imagePullSecrets doesn't have 'docker-password' key.", secretName))
return false, nil
} }
if secret.Data["docker-email"] == nil { if secret.Data["docker-email"] == nil {
r.logger.V(log.VWarn).Info(fmt.Sprintf("Secret '%s' defined in spec.master.imagePullSecrets doesn't have 'docker-email' key.", secretName)) messages = append(messages, fmt.Sprintf("Secret '%s' defined in spec.master.imagePullSecrets doesn't have 'docker-email' key.", secretName))
return false, nil
} }
return true, nil return messages, nil
} }
func (r *ReconcileJenkinsBaseConfiguration) validateVolumes() (bool, error) { func (r *ReconcileJenkinsBaseConfiguration) validateVolumes() ([]string, error) {
valid := true var messages []string
for _, volume := range r.jenkins.Spec.Master.Volumes { for _, volume := range r.jenkins.Spec.Master.Volumes {
switch { switch {
case volume.ConfigMap != nil: case volume.ConfigMap != nil:
if ok, err := r.validateConfigMapVolume(volume); err != nil { if msg, err := r.validateConfigMapVolume(volume); err != nil {
return false, err return nil, err
} else if !ok { } else if len(msg) > 0 {
valid = false messages = append(messages, msg...)
} }
case volume.Secret != nil: case volume.Secret != nil:
if ok, err := r.validateSecretVolume(volume); err != nil { if msg, err := r.validateSecretVolume(volume); err != nil {
return false, err return nil, err
} else if !ok { } else if len(msg) > 0 {
valid = false messages = append(messages, msg...)
} }
case volume.PersistentVolumeClaim != nil: case volume.PersistentVolumeClaim != nil:
if ok, err := r.validatePersistentVolumeClaim(volume); err != nil { if msg, err := r.validatePersistentVolumeClaim(volume); err != nil {
return false, err return nil, err
} else if !ok { } else if len(msg) > 0 {
valid = false messages = append(messages, msg...)
} }
default: //TODO add support for rest of volumes default: //TODO add support for rest of volumes
valid = false messages = append(messages, fmt.Sprintf("Unsupported volume '%v'", volume))
r.logger.V(log.VWarn).Info(fmt.Sprintf("Unsupported volume '%v'", volume))
} }
} }
return valid, nil return messages, nil
} }
func (r *ReconcileJenkinsBaseConfiguration) validatePersistentVolumeClaim(volume corev1.Volume) (bool, error) { func (r *ReconcileJenkinsBaseConfiguration) validatePersistentVolumeClaim(volume corev1.Volume) ([]string, error) {
var messages []string
pvc := &corev1.PersistentVolumeClaim{} pvc := &corev1.PersistentVolumeClaim{}
err := r.k8sClient.Get(context.TODO(), types.NamespacedName{Name: volume.PersistentVolumeClaim.ClaimName, Namespace: r.jenkins.ObjectMeta.Namespace}, pvc) err := r.k8sClient.Get(context.TODO(), types.NamespacedName{Name: volume.PersistentVolumeClaim.ClaimName, Namespace: r.jenkins.ObjectMeta.Namespace}, pvc)
if err != nil && apierrors.IsNotFound(err) { if err != nil && apierrors.IsNotFound(err) {
r.logger.V(log.VWarn).Info(fmt.Sprintf("PersistentVolumeClaim '%s' not found for volume '%v'", volume.PersistentVolumeClaim.ClaimName, volume)) messages = append(messages, fmt.Sprintf("PersistentVolumeClaim '%s' not found for volume '%v'", volume.PersistentVolumeClaim.ClaimName, volume))
return false, nil
} else if err != nil && !apierrors.IsNotFound(err) { } else if err != nil && !apierrors.IsNotFound(err) {
return false, stackerr.WithStack(err) return nil, stackerr.WithStack(err)
} }
return true, nil return messages, nil
} }
func (r *ReconcileJenkinsBaseConfiguration) validateConfigMapVolume(volume corev1.Volume) (bool, error) { func (r *ReconcileJenkinsBaseConfiguration) validateConfigMapVolume(volume corev1.Volume) ([]string, error) {
var messages []string
if volume.ConfigMap.Optional != nil && *volume.ConfigMap.Optional { if volume.ConfigMap.Optional != nil && *volume.ConfigMap.Optional {
return true, nil return nil, nil
} }
configMap := &corev1.ConfigMap{} configMap := &corev1.ConfigMap{}
err := r.k8sClient.Get(context.TODO(), types.NamespacedName{Name: volume.ConfigMap.Name, Namespace: r.jenkins.ObjectMeta.Namespace}, configMap) err := r.k8sClient.Get(context.TODO(), types.NamespacedName{Name: volume.ConfigMap.Name, Namespace: r.jenkins.ObjectMeta.Namespace}, configMap)
if err != nil && apierrors.IsNotFound(err) { if err != nil && apierrors.IsNotFound(err) {
r.logger.V(log.VWarn).Info(fmt.Sprintf("ConfigMap '%s' not found for volume '%v'", volume.ConfigMap.Name, volume)) messages = append(messages, fmt.Sprintf("ConfigMap '%s' not found for volume '%v'", volume.ConfigMap.Name, volume))
return false, nil
} else if err != nil && !apierrors.IsNotFound(err) { } else if err != nil && !apierrors.IsNotFound(err) {
return false, stackerr.WithStack(err) return nil, stackerr.WithStack(err)
} }
return true, nil return messages, nil
} }
func (r *ReconcileJenkinsBaseConfiguration) validateSecretVolume(volume corev1.Volume) (bool, error) { func (r *ReconcileJenkinsBaseConfiguration) validateSecretVolume(volume corev1.Volume) ([]string, error) {
var messages []string
if volume.Secret.Optional != nil && *volume.Secret.Optional { if volume.Secret.Optional != nil && *volume.Secret.Optional {
return true, nil return nil, nil
} }
secret := &corev1.Secret{} secret := &corev1.Secret{}
err := r.k8sClient.Get(context.TODO(), types.NamespacedName{Name: volume.Secret.SecretName, Namespace: r.jenkins.ObjectMeta.Namespace}, secret) err := r.k8sClient.Get(context.TODO(), types.NamespacedName{Name: volume.Secret.SecretName, Namespace: r.jenkins.ObjectMeta.Namespace}, secret)
if err != nil && apierrors.IsNotFound(err) { if err != nil && apierrors.IsNotFound(err) {
r.logger.V(log.VWarn).Info(fmt.Sprintf("Secret '%s' not found for volume '%v'", volume.Secret.SecretName, volume)) messages = append(messages, fmt.Sprintf("Secret '%s' not found for volume '%v'", volume.Secret.SecretName, volume))
return false, nil
} else if err != nil && !apierrors.IsNotFound(err) { } else if err != nil && !apierrors.IsNotFound(err) {
return false, stackerr.WithStack(err) return nil, stackerr.WithStack(err)
} }
return true, nil return messages, nil
} }
func (r *ReconcileJenkinsBaseConfiguration) validateReservedVolumes() bool { func (r *ReconcileJenkinsBaseConfiguration) validateReservedVolumes() []string {
valid := true var messages []string
for _, baseVolume := range resources.GetJenkinsMasterPodBaseVolumes(r.jenkins) { for _, baseVolume := range resources.GetJenkinsMasterPodBaseVolumes(r.jenkins) {
for _, volume := range r.jenkins.Spec.Master.Volumes { for _, volume := range r.jenkins.Spec.Master.Volumes {
if baseVolume.Name == volume.Name { if baseVolume.Name == volume.Name {
r.logger.V(log.VWarn).Info(fmt.Sprintf("Jenkins Master pod volume '%s' is reserved please choose different one", volume.Name)) messages = append(messages, fmt.Sprintf("Jenkins Master pod volume '%s' is reserved please choose different one", volume.Name))
valid = false
} }
} }
} }
return valid return messages
} }
func (r *ReconcileJenkinsBaseConfiguration) validateContainer(container v1alpha2.Container) bool { func (r *ReconcileJenkinsBaseConfiguration) validateContainer(container v1alpha2.Container) []string {
logger := r.logger.WithValues("container", container.Name) var messages []string
if container.Image == "" { if container.Image == "" {
logger.V(log.VWarn).Info("Image not set") messages = append(messages, "Image not set")
return false
} }
if !dockerImageRegexp.MatchString(container.Image) && !docker.ReferenceRegexp.MatchString(container.Image) { if !dockerImageRegexp.MatchString(container.Image) && !docker.ReferenceRegexp.MatchString(container.Image) {
logger.V(log.VWarn).Info("Invalid image") messages = append(messages, "Invalid image")
return false
} }
if container.ImagePullPolicy == "" { if container.ImagePullPolicy == "" {
logger.V(log.VWarn).Info("Image pull policy not set") messages = append(messages, "Image pull policy not set")
return false
} }
if !r.validateContainerVolumeMounts(container) { if msg := r.validateContainerVolumeMounts(container); len(msg) > 0 {
return false messages = append(messages, msg...)
} }
return true return messages
} }
func (r *ReconcileJenkinsBaseConfiguration) validateContainerVolumeMounts(container v1alpha2.Container) bool { func (r *ReconcileJenkinsBaseConfiguration) validateContainerVolumeMounts(container v1alpha2.Container) []string {
logger := r.logger.WithValues("container", container.Name) var messages []string
allVolumes := append(resources.GetJenkinsMasterPodBaseVolumes(r.jenkins), r.jenkins.Spec.Master.Volumes...) allVolumes := append(resources.GetJenkinsMasterPodBaseVolumes(r.jenkins), r.jenkins.Spec.Master.Volumes...)
valid := true
for _, volumeMount := range container.VolumeMounts { for _, volumeMount := range container.VolumeMounts {
if len(volumeMount.MountPath) == 0 { if len(volumeMount.MountPath) == 0 {
logger.V(log.VWarn).Info(fmt.Sprintf("mountPath not set for '%s' volume mount in container '%s'", volumeMount.Name, container.Name)) messages = append(messages, fmt.Sprintf("mountPath not set for '%s' volume mount in container '%s'", volumeMount.Name, container.Name))
valid = false
} }
foundVolume := false foundVolume := false
@ -241,15 +235,15 @@ func (r *ReconcileJenkinsBaseConfiguration) validateContainerVolumeMounts(contai
} }
if !foundVolume { if !foundVolume {
logger.V(log.VWarn).Info(fmt.Sprintf("Not found volume for '%s' volume mount in container '%s'", volumeMount.Name, container.Name)) messages = append(messages, fmt.Sprintf("Not found volume for '%s' volume mount in container '%s'", volumeMount.Name, container.Name))
valid = false
} }
} }
return valid return messages
} }
func (r *ReconcileJenkinsBaseConfiguration) validateJenkinsMasterPodEnvs() bool { func (r *ReconcileJenkinsBaseConfiguration) validateJenkinsMasterPodEnvs() []string {
var messages []string
baseEnvs := resources.GetJenkinsMasterContainerBaseEnvs(r.jenkins) baseEnvs := resources.GetJenkinsMasterContainerBaseEnvs(r.jenkins)
baseEnvNames := map[string]string{} baseEnvNames := map[string]string{}
for _, env := range baseEnvs { for _, env := range baseEnvs {
@ -257,14 +251,12 @@ func (r *ReconcileJenkinsBaseConfiguration) validateJenkinsMasterPodEnvs() bool
} }
javaOpts := corev1.EnvVar{} javaOpts := corev1.EnvVar{}
valid := true
for _, userEnv := range r.jenkins.Spec.Master.Containers[0].Env { for _, userEnv := range r.jenkins.Spec.Master.Containers[0].Env {
if userEnv.Name == constants.JavaOpsVariableName { if userEnv.Name == constants.JavaOpsVariableName {
javaOpts = userEnv javaOpts = userEnv
} }
if _, overriding := baseEnvNames[userEnv.Name]; overriding { if _, overriding := baseEnvNames[userEnv.Name]; overriding {
r.logger.V(log.VWarn).Info(fmt.Sprintf("Jenkins Master container env '%s' cannot be overridden", userEnv.Name)) messages = append(messages, fmt.Sprintf("Jenkins Master container env '%s' cannot be overridden", userEnv.Name))
valid = false
} }
} }
@ -282,23 +274,21 @@ func (r *ReconcileJenkinsBaseConfiguration) validateJenkinsMasterPodEnvs() bool
} }
for requiredFlag, set := range requiredFlags { for requiredFlag, set := range requiredFlags {
if !set { if !set {
valid = false messages = append(messages, fmt.Sprintf("Jenkins Master container env '%s' doesn't have required flag '%s'", constants.JavaOpsVariableName, requiredFlag))
r.logger.V(log.VWarn).Info(fmt.Sprintf("Jenkins Master container env '%s' doesn't have required flag '%s'", constants.JavaOpsVariableName, requiredFlag))
} }
} }
return valid return messages
} }
func (r *ReconcileJenkinsBaseConfiguration) validatePlugins(requiredBasePlugins []plugins.Plugin, basePlugins, userPlugins []v1alpha2.Plugin) bool { func (r *ReconcileJenkinsBaseConfiguration) validatePlugins(requiredBasePlugins []plugins.Plugin, basePlugins, userPlugins []v1alpha2.Plugin) []string {
valid := true var messages []string
allPlugins := map[plugins.Plugin][]plugins.Plugin{} allPlugins := map[plugins.Plugin][]plugins.Plugin{}
for _, jenkinsPlugin := range basePlugins { for _, jenkinsPlugin := range basePlugins {
plugin, err := plugins.NewPlugin(jenkinsPlugin.Name, jenkinsPlugin.Version) plugin, err := plugins.NewPlugin(jenkinsPlugin.Name, jenkinsPlugin.Version)
if err != nil { if err != nil {
r.logger.V(log.VWarn).Info(err.Error()) messages = append(messages, err.Error())
valid = false
} }
if plugin != nil { if plugin != nil {
@ -309,8 +299,7 @@ func (r *ReconcileJenkinsBaseConfiguration) validatePlugins(requiredBasePlugins
for _, jenkinsPlugin := range userPlugins { for _, jenkinsPlugin := range userPlugins {
plugin, err := plugins.NewPlugin(jenkinsPlugin.Name, jenkinsPlugin.Version) plugin, err := plugins.NewPlugin(jenkinsPlugin.Name, jenkinsPlugin.Version)
if err != nil { if err != nil {
r.logger.V(log.VWarn).Info(err.Error()) messages = append(messages, err.Error())
valid = false
} }
if plugin != nil { if plugin != nil {
@ -318,19 +307,19 @@ func (r *ReconcileJenkinsBaseConfiguration) validatePlugins(requiredBasePlugins
} }
} }
if !plugins.VerifyDependencies(allPlugins) { if msg := plugins.VerifyDependencies(allPlugins); len(msg) > 0 {
valid = false messages = append(messages, msg...)
} }
if !r.verifyBasePlugins(requiredBasePlugins, basePlugins) { if msg := r.verifyBasePlugins(requiredBasePlugins, basePlugins); len(msg) > 0 {
valid = false messages = append(messages, msg...)
} }
return valid return messages
} }
func (r *ReconcileJenkinsBaseConfiguration) verifyBasePlugins(requiredBasePlugins []plugins.Plugin, basePlugins []v1alpha2.Plugin) bool { func (r *ReconcileJenkinsBaseConfiguration) verifyBasePlugins(requiredBasePlugins []plugins.Plugin, basePlugins []v1alpha2.Plugin) []string {
valid := true var messages []string
for _, requiredBasePlugin := range requiredBasePlugins { for _, requiredBasePlugin := range requiredBasePlugins {
found := false found := false
@ -341,52 +330,46 @@ func (r *ReconcileJenkinsBaseConfiguration) verifyBasePlugins(requiredBasePlugin
} }
} }
if !found { if !found {
valid = false messages = append(messages, fmt.Sprintf("Missing plugin '%s' in spec.master.basePlugins", requiredBasePlugin.Name))
r.logger.V(log.VWarn).Info(fmt.Sprintf("Missing plugin '%s' in spec.master.basePlugins", requiredBasePlugin.Name))
} }
} }
return valid return messages
} }
func (r *ReconcileJenkinsBaseConfiguration) validateCustomization(customization v1alpha2.Customization, name string) (bool, error) { func (r *ReconcileJenkinsBaseConfiguration) validateCustomization(customization v1alpha2.Customization, name string) ([]string, error) {
valid := true var messages []string
if len(customization.Secret.Name) == 0 && len(customization.Configurations) == 0 { if len(customization.Secret.Name) == 0 && len(customization.Configurations) == 0 {
return true, nil return nil, nil
} }
if len(customization.Secret.Name) > 0 && len(customization.Configurations) == 0 { if len(customization.Secret.Name) > 0 && len(customization.Configurations) == 0 {
valid = false messages = append(messages, fmt.Sprintf("%s.secret.name is set but %s.configurations is empty", name, name))
r.logger.V(log.VWarn).Info(fmt.Sprintf("%s.secret.name is set but %s.configurations is empty", name, name))
} }
if len(customization.Secret.Name) > 0 { if len(customization.Secret.Name) > 0 {
secret := &corev1.Secret{} secret := &corev1.Secret{}
err := r.k8sClient.Get(context.TODO(), types.NamespacedName{Name: customization.Secret.Name, Namespace: r.jenkins.ObjectMeta.Namespace}, secret) err := r.k8sClient.Get(context.TODO(), types.NamespacedName{Name: customization.Secret.Name, Namespace: r.jenkins.ObjectMeta.Namespace}, secret)
if err != nil && apierrors.IsNotFound(err) { if err != nil && apierrors.IsNotFound(err) {
valid = false messages = append(messages, fmt.Sprintf("Secret '%s' configured in %s.secret.name not found", customization.Secret.Name, name))
r.logger.V(log.VWarn).Info(fmt.Sprintf("Secret '%s' configured in %s.secret.name not found", customization.Secret.Name, name))
} else if err != nil && !apierrors.IsNotFound(err) { } else if err != nil && !apierrors.IsNotFound(err) {
return false, stackerr.WithStack(err) return nil, stackerr.WithStack(err)
} }
} }
for index, configMapRef := range customization.Configurations { for index, configMapRef := range customization.Configurations {
if len(configMapRef.Name) == 0 { if len(configMapRef.Name) == 0 {
r.logger.V(log.VWarn).Info(fmt.Sprintf("%s.configurations[%d] name is empty", name, index)) messages = append(messages, fmt.Sprintf("%s.configurations[%d] name is empty", name, index))
valid = false
continue continue
} }
configMap := &corev1.ConfigMap{} configMap := &corev1.ConfigMap{}
err := r.k8sClient.Get(context.TODO(), types.NamespacedName{Name: configMapRef.Name, Namespace: r.jenkins.ObjectMeta.Namespace}, configMap) err := r.k8sClient.Get(context.TODO(), types.NamespacedName{Name: configMapRef.Name, Namespace: r.jenkins.ObjectMeta.Namespace}, configMap)
if err != nil && apierrors.IsNotFound(err) { if err != nil && apierrors.IsNotFound(err) {
valid = false messages = append(messages, fmt.Sprintf("ConfigMap '%s' configured in %s.configurations[%d] not found", configMapRef.Name, name, index))
r.logger.V(log.VWarn).Info(fmt.Sprintf("ConfigMap '%s' configured in %s.configurations[%d] not found", configMapRef.Name, name, index))
return false, nil
} else if err != nil && !apierrors.IsNotFound(err) { } else if err != nil && !apierrors.IsNotFound(err) {
return false, stackerr.WithStack(err) return nil, stackerr.WithStack(err)
} }
} }
return valid, nil return messages, nil
} }

View File

@ -22,7 +22,7 @@ import (
func TestValidatePlugins(t *testing.T) { func TestValidatePlugins(t *testing.T) {
log.SetupLogger(true) log.SetupLogger(true)
baseReconcileLoop := New(nil, nil, log.Log, baseReconcileLoop := New(nil, nil, log.Log,
nil, false, false, nil, nil) nil, false, false, nil, nil, nil)
t.Run("empty", func(t *testing.T) { t.Run("empty", func(t *testing.T) {
var requiredBasePlugins []plugins.Plugin var requiredBasePlugins []plugins.Plugin
var basePlugins []v1alpha2.Plugin var basePlugins []v1alpha2.Plugin
@ -30,7 +30,7 @@ func TestValidatePlugins(t *testing.T) {
got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins) got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins)
assert.True(t, got) assert.Nil(t, got)
}) })
t.Run("valid user plugin", func(t *testing.T) { t.Run("valid user plugin", func(t *testing.T) {
var requiredBasePlugins []plugins.Plugin var requiredBasePlugins []plugins.Plugin
@ -39,7 +39,7 @@ func TestValidatePlugins(t *testing.T) {
got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins) got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins)
assert.True(t, got) assert.Nil(t, got)
}) })
t.Run("invalid user plugin name", func(t *testing.T) { t.Run("invalid user plugin name", func(t *testing.T) {
var requiredBasePlugins []plugins.Plugin var requiredBasePlugins []plugins.Plugin
@ -48,7 +48,7 @@ func TestValidatePlugins(t *testing.T) {
got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins) got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins)
assert.False(t, got) assert.Equal(t, got, []string{"invalid plugin name 'INVALID?:0.0.1', must follow pattern '(?i)^[0-9a-z-_]+$'"})
}) })
t.Run("invalid user plugin version", func(t *testing.T) { t.Run("invalid user plugin version", func(t *testing.T) {
var requiredBasePlugins []plugins.Plugin var requiredBasePlugins []plugins.Plugin
@ -66,7 +66,7 @@ func TestValidatePlugins(t *testing.T) {
got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins) got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins)
assert.True(t, got) assert.Nil(t, got)
}) })
t.Run("invalid base plugin name", func(t *testing.T) { t.Run("invalid base plugin name", func(t *testing.T) {
var requiredBasePlugins []plugins.Plugin var requiredBasePlugins []plugins.Plugin
@ -75,7 +75,7 @@ func TestValidatePlugins(t *testing.T) {
got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins) got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins)
assert.False(t, got) assert.Equal(t, got, []string{"invalid plugin name 'INVALID?:0.0.1', must follow pattern '(?i)^[0-9a-z-_]+$'"})
}) })
t.Run("invalid base plugin version", func(t *testing.T) { t.Run("invalid base plugin version", func(t *testing.T) {
var requiredBasePlugins []plugins.Plugin var requiredBasePlugins []plugins.Plugin
@ -93,7 +93,7 @@ func TestValidatePlugins(t *testing.T) {
got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins) got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins)
assert.True(t, got) assert.Nil(t, got)
}) })
t.Run("invalid user and base plugin version", func(t *testing.T) { t.Run("invalid user and base plugin version", func(t *testing.T) {
var requiredBasePlugins []plugins.Plugin var requiredBasePlugins []plugins.Plugin
@ -102,7 +102,8 @@ func TestValidatePlugins(t *testing.T) {
got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins) got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins)
assert.False(t, got) 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) { t.Run("required base plugin set with the same version", func(t *testing.T) {
requiredBasePlugins := []plugins.Plugin{{Name: "simple-plugin", Version: "0.0.1"}} requiredBasePlugins := []plugins.Plugin{{Name: "simple-plugin", Version: "0.0.1"}}
@ -111,7 +112,7 @@ func TestValidatePlugins(t *testing.T) {
got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins) got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins)
assert.True(t, got) assert.Nil(t, got)
}) })
t.Run("required base plugin set with different version", func(t *testing.T) { t.Run("required base plugin set with different version", func(t *testing.T) {
requiredBasePlugins := []plugins.Plugin{{Name: "simple-plugin", Version: "0.0.1"}} requiredBasePlugins := []plugins.Plugin{{Name: "simple-plugin", Version: "0.0.1"}}
@ -120,16 +121,16 @@ func TestValidatePlugins(t *testing.T) {
got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins) got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins)
assert.True(t, got) assert.Nil(t, got)
}) })
t.Run("missign required base plugin", func(t *testing.T) { t.Run("missing required base plugin", func(t *testing.T) {
requiredBasePlugins := []plugins.Plugin{{Name: "simple-plugin", Version: "0.0.1"}} requiredBasePlugins := []plugins.Plugin{{Name: "simple-plugin", Version: "0.0.1"}}
var basePlugins []v1alpha2.Plugin var basePlugins []v1alpha2.Plugin
var userPlugins []v1alpha2.Plugin var userPlugins []v1alpha2.Plugin
got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins) got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins)
assert.False(t, got) assert.Equal(t, got, []string{"Missing plugin 'simple-plugin' in spec.master.basePlugins"})
}) })
} }
@ -162,10 +163,10 @@ func TestReconcileJenkinsBaseConfiguration_validateImagePullSecrets(t *testing.T
assert.NoError(t, err) assert.NoError(t, err)
baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false),
&jenkins, false, false, nil, nil) &jenkins, false, false, nil, nil, nil)
got, err := baseReconcileLoop.validateImagePullSecrets() got, err := baseReconcileLoop.validateImagePullSecrets()
assert.Equal(t, got, true) assert.Nil(t, got)
assert.NoError(t, err) assert.NoError(t, err)
}) })
@ -183,10 +184,11 @@ func TestReconcileJenkinsBaseConfiguration_validateImagePullSecrets(t *testing.T
fakeClient := fake.NewFakeClient() fakeClient := fake.NewFakeClient()
baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false),
&jenkins, false, false, nil, nil) &jenkins, false, false, nil, nil, nil)
got, _ := baseReconcileLoop.validateImagePullSecrets() got, _ := baseReconcileLoop.validateImagePullSecrets()
assert.Equal(t, got, false)
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) { t.Run("no docker email", func(t *testing.T) {
@ -216,10 +218,11 @@ func TestReconcileJenkinsBaseConfiguration_validateImagePullSecrets(t *testing.T
assert.NoError(t, err) assert.NoError(t, err)
baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false),
&jenkins, false, false, nil, nil) &jenkins, false, false, nil, nil, nil)
got, _ := baseReconcileLoop.validateImagePullSecrets() got, _ := baseReconcileLoop.validateImagePullSecrets()
assert.Equal(t, got, false)
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) { t.Run("no docker password", func(t *testing.T) {
@ -249,10 +252,11 @@ func TestReconcileJenkinsBaseConfiguration_validateImagePullSecrets(t *testing.T
assert.NoError(t, err) assert.NoError(t, err)
baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false),
&jenkins, false, false, nil, nil) &jenkins, false, false, nil, nil, nil)
got, _ := baseReconcileLoop.validateImagePullSecrets() got, _ := baseReconcileLoop.validateImagePullSecrets()
assert.Equal(t, got, false)
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) { t.Run("no docker username", func(t *testing.T) {
@ -282,10 +286,11 @@ func TestReconcileJenkinsBaseConfiguration_validateImagePullSecrets(t *testing.T
assert.NoError(t, err) assert.NoError(t, err)
baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false),
&jenkins, false, false, nil, nil) &jenkins, false, false, nil, nil, nil)
got, _ := baseReconcileLoop.validateImagePullSecrets() got, _ := baseReconcileLoop.validateImagePullSecrets()
assert.Equal(t, got, false)
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) { t.Run("no docker server", func(t *testing.T) {
@ -315,10 +320,11 @@ func TestReconcileJenkinsBaseConfiguration_validateImagePullSecrets(t *testing.T
assert.NoError(t, err) assert.NoError(t, err)
baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false),
&jenkins, false, false, nil, nil) &jenkins, false, false, nil, nil, nil)
got, _ := baseReconcileLoop.validateImagePullSecrets() got, _ := baseReconcileLoop.validateImagePullSecrets()
assert.Equal(t, got, false)
assert.Equal(t, got, []string{"Secret 'test-ref' defined in spec.master.imagePullSecrets doesn't have 'docker-server' key."})
}) })
} }
@ -346,9 +352,9 @@ func TestValidateJenkinsMasterPodEnvs(t *testing.T) {
}, },
} }
baseReconcileLoop := New(nil, nil, logf.ZapLogger(false), baseReconcileLoop := New(nil, nil, logf.ZapLogger(false),
&jenkins, false, false, nil, nil) &jenkins, false, false, nil, nil, nil)
got := baseReconcileLoop.validateJenkinsMasterPodEnvs() got := baseReconcileLoop.validateJenkinsMasterPodEnvs()
assert.Equal(t, true, got) assert.Nil(t, got)
}) })
t.Run("override JENKINS_HOME env", func(t *testing.T) { t.Run("override JENKINS_HOME env", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{ jenkins := v1alpha2.Jenkins{
@ -372,9 +378,10 @@ func TestValidateJenkinsMasterPodEnvs(t *testing.T) {
}, },
} }
baseReconcileLoop := New(nil, nil, logf.ZapLogger(false), baseReconcileLoop := New(nil, nil, logf.ZapLogger(false),
&jenkins, false, false, nil, nil) &jenkins, false, false, nil, nil, nil)
got := baseReconcileLoop.validateJenkinsMasterPodEnvs() got := baseReconcileLoop.validateJenkinsMasterPodEnvs()
assert.Equal(t, false, got)
assert.Equal(t, got, []string{"Jenkins Master container env 'JENKINS_HOME' cannot be overridden"})
}) })
t.Run("missing -Djava.awt.headless=true in JAVA_OPTS env", func(t *testing.T) { t.Run("missing -Djava.awt.headless=true in JAVA_OPTS env", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{ jenkins := v1alpha2.Jenkins{
@ -394,9 +401,10 @@ func TestValidateJenkinsMasterPodEnvs(t *testing.T) {
}, },
} }
baseReconcileLoop := New(nil, nil, logf.ZapLogger(false), baseReconcileLoop := New(nil, nil, logf.ZapLogger(false),
&jenkins, false, false, nil, nil) &jenkins, false, false, nil, nil, nil)
got := baseReconcileLoop.validateJenkinsMasterPodEnvs() got := baseReconcileLoop.validateJenkinsMasterPodEnvs()
assert.Equal(t, false, got)
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) { t.Run("missing -Djenkins.install.runSetupWizard=false in JAVA_OPTS env", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{ jenkins := v1alpha2.Jenkins{
@ -416,9 +424,10 @@ func TestValidateJenkinsMasterPodEnvs(t *testing.T) {
}, },
} }
baseReconcileLoop := New(nil, nil, logf.ZapLogger(false), baseReconcileLoop := New(nil, nil, logf.ZapLogger(false),
&jenkins, false, false, nil, nil) &jenkins, false, false, nil, nil, nil)
got := baseReconcileLoop.validateJenkinsMasterPodEnvs() got := baseReconcileLoop.validateJenkinsMasterPodEnvs()
assert.Equal(t, false, got)
assert.Equal(t, got, []string{"Jenkins Master container env 'JAVA_OPTS' doesn't have required flag '-Djenkins.install.runSetupWizard=false'"})
}) })
} }
@ -436,9 +445,9 @@ func TestValidateReservedVolumes(t *testing.T) {
}, },
} }
baseReconcileLoop := New(nil, nil, logf.ZapLogger(false), baseReconcileLoop := New(nil, nil, logf.ZapLogger(false),
&jenkins, false, false, nil, nil) &jenkins, false, false, nil, nil, nil)
got := baseReconcileLoop.validateReservedVolumes() got := baseReconcileLoop.validateReservedVolumes()
assert.Equal(t, true, got) assert.Nil(t, got)
}) })
t.Run("used reserved name", func(t *testing.T) { t.Run("used reserved name", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{ jenkins := v1alpha2.Jenkins{
@ -453,9 +462,10 @@ func TestValidateReservedVolumes(t *testing.T) {
}, },
} }
baseReconcileLoop := New(nil, nil, logf.ZapLogger(false), baseReconcileLoop := New(nil, nil, logf.ZapLogger(false),
&jenkins, false, false, nil, nil) &jenkins, false, false, nil, nil, nil)
got := baseReconcileLoop.validateReservedVolumes() got := baseReconcileLoop.validateReservedVolumes()
assert.Equal(t, false, got)
assert.Equal(t, got, []string{"Jenkins Master pod volume 'jenkins-home' is reserved please choose different one"})
}) })
} }
@ -467,9 +477,9 @@ func TestValidateContainerVolumeMounts(t *testing.T) {
}, },
} }
baseReconcileLoop := New(nil, nil, logf.ZapLogger(false), baseReconcileLoop := New(nil, nil, logf.ZapLogger(false),
&jenkins, false, false, nil, nil) &jenkins, false, false, nil, nil, nil)
got := baseReconcileLoop.validateContainerVolumeMounts(v1alpha2.Container{}) got := baseReconcileLoop.validateContainerVolumeMounts(v1alpha2.Container{})
assert.Equal(t, true, got) assert.Nil(t, got)
}) })
t.Run("one extra volume", func(t *testing.T) { t.Run("one extra volume", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{ jenkins := v1alpha2.Jenkins{
@ -494,9 +504,9 @@ func TestValidateContainerVolumeMounts(t *testing.T) {
}, },
} }
baseReconcileLoop := New(nil, nil, logf.ZapLogger(false), baseReconcileLoop := New(nil, nil, logf.ZapLogger(false),
&jenkins, false, false, nil, nil) &jenkins, false, false, nil, nil, nil)
got := baseReconcileLoop.validateContainerVolumeMounts(jenkins.Spec.Master.Containers[0]) got := baseReconcileLoop.validateContainerVolumeMounts(jenkins.Spec.Master.Containers[0])
assert.Equal(t, true, got) assert.Nil(t, got)
}) })
t.Run("empty mountPath", func(t *testing.T) { t.Run("empty mountPath", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{ jenkins := v1alpha2.Jenkins{
@ -521,9 +531,9 @@ func TestValidateContainerVolumeMounts(t *testing.T) {
}, },
} }
baseReconcileLoop := New(nil, nil, logf.ZapLogger(false), baseReconcileLoop := New(nil, nil, logf.ZapLogger(false),
&jenkins, false, false, nil, nil) &jenkins, false, false, nil, nil, nil)
got := baseReconcileLoop.validateContainerVolumeMounts(jenkins.Spec.Master.Containers[0]) got := baseReconcileLoop.validateContainerVolumeMounts(jenkins.Spec.Master.Containers[0])
assert.Equal(t, false, got) assert.Equal(t, got, []string{"mountPath not set for 'example' volume mount in container ''"})
}) })
t.Run("missing volume", func(t *testing.T) { t.Run("missing volume", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{ jenkins := v1alpha2.Jenkins{
@ -543,9 +553,10 @@ func TestValidateContainerVolumeMounts(t *testing.T) {
}, },
} }
baseReconcileLoop := New(nil, nil, logf.ZapLogger(false), baseReconcileLoop := New(nil, nil, logf.ZapLogger(false),
&jenkins, false, false, nil, nil) &jenkins, false, false, nil, nil, nil)
got := baseReconcileLoop.validateContainerVolumeMounts(jenkins.Spec.Master.Containers[0]) got := baseReconcileLoop.validateContainerVolumeMounts(jenkins.Spec.Master.Containers[0])
assert.Equal(t, false, got)
assert.Equal(t, got, []string{"Not found volume for 'missing-volume' volume mount in container ''"})
}) })
} }
@ -563,12 +574,12 @@ func TestValidateConfigMapVolume(t *testing.T) {
} }
fakeClient := fake.NewFakeClient() fakeClient := fake.NewFakeClient()
baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false),
nil, false, false, nil, nil) nil, false, false, nil, nil, nil)
got, err := baseReconcileLoop.validateConfigMapVolume(volume) got, err := baseReconcileLoop.validateConfigMapVolume(volume)
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, got) assert.Nil(t, got)
}) })
t.Run("happy, required", func(t *testing.T) { t.Run("happy, required", func(t *testing.T) {
optional := false optional := false
@ -589,12 +600,12 @@ func TestValidateConfigMapVolume(t *testing.T) {
err := fakeClient.Create(context.TODO(), &configMap) err := fakeClient.Create(context.TODO(), &configMap)
assert.NoError(t, err) assert.NoError(t, err)
baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false),
jenkins, false, false, nil, nil) jenkins, false, false, nil, nil, nil)
got, err := baseReconcileLoop.validateConfigMapVolume(volume) got, err := baseReconcileLoop.validateConfigMapVolume(volume)
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, got) assert.Nil(t, got)
}) })
t.Run("missing configmap", func(t *testing.T) { t.Run("missing configmap", func(t *testing.T) {
optional := false optional := false
@ -613,12 +624,13 @@ func TestValidateConfigMapVolume(t *testing.T) {
} }
fakeClient := fake.NewFakeClient() fakeClient := fake.NewFakeClient()
baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false),
jenkins, false, false, nil, nil) jenkins, false, false, nil, nil, nil)
got, err := baseReconcileLoop.validateConfigMapVolume(volume) got, err := baseReconcileLoop.validateConfigMapVolume(volume)
assert.NoError(t, err) assert.NoError(t, err)
assert.False(t, got)
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:[],DefaultMode:nil,Optional:*false,} nil nil nil nil nil nil nil nil}}'"})
}) })
} }
@ -636,12 +648,12 @@ func TestValidateSecretVolume(t *testing.T) {
} }
fakeClient := fake.NewFakeClient() fakeClient := fake.NewFakeClient()
baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false),
nil, false, false, nil, nil) nil, false, false, nil, nil, nil)
got, err := baseReconcileLoop.validateSecretVolume(volume) got, err := baseReconcileLoop.validateSecretVolume(volume)
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, got) assert.Nil(t, got)
}) })
t.Run("happy, required", func(t *testing.T) { t.Run("happy, required", func(t *testing.T) {
optional := false optional := false
@ -660,12 +672,12 @@ func TestValidateSecretVolume(t *testing.T) {
err := fakeClient.Create(context.TODO(), &secret) err := fakeClient.Create(context.TODO(), &secret)
assert.NoError(t, err) assert.NoError(t, err)
baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false),
jenkins, false, false, nil, nil) jenkins, false, false, nil, nil, nil)
got, err := baseReconcileLoop.validateSecretVolume(volume) got, err := baseReconcileLoop.validateSecretVolume(volume)
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, got) assert.Nil(t, got)
}) })
t.Run("missing secret", func(t *testing.T) { t.Run("missing secret", func(t *testing.T) {
optional := false optional := false
@ -682,12 +694,13 @@ func TestValidateSecretVolume(t *testing.T) {
} }
fakeClient := fake.NewFakeClient() fakeClient := fake.NewFakeClient()
baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false),
jenkins, false, false, nil, nil) jenkins, false, false, nil, nil, nil)
got, err := baseReconcileLoop.validateSecretVolume(volume) got, err := baseReconcileLoop.validateSecretVolume(volume)
assert.NoError(t, err) assert.NoError(t, err)
assert.False(t, got)
assert.Equal(t, got, []string{"Secret 'secret-name' not found for volume '{volume-name {nil nil nil nil nil &SecretVolumeSource{SecretName:secret-name,Items:[],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}}'"})
}) })
} }
@ -704,12 +717,12 @@ func TestValidateCustomization(t *testing.T) {
customization := v1alpha2.Customization{} customization := v1alpha2.Customization{}
fakeClient := fake.NewFakeClient() fakeClient := fake.NewFakeClient()
baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false),
jenkins, false, false, nil, nil) jenkins, false, false, nil, nil, nil)
got, err := baseReconcileLoop.validateCustomization(customization, "spec.groovyScripts") got, err := baseReconcileLoop.validateCustomization(customization, "spec.groovyScripts")
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, got) assert.Nil(t, got)
}) })
t.Run("secret set but configurations is empty", func(t *testing.T) { t.Run("secret set but configurations is empty", func(t *testing.T) {
customization := v1alpha2.Customization{ customization := v1alpha2.Customization{
@ -724,14 +737,15 @@ func TestValidateCustomization(t *testing.T) {
} }
fakeClient := fake.NewFakeClient() fakeClient := fake.NewFakeClient()
baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false),
jenkins, false, false, nil, nil) jenkins, false, false, nil, nil, nil)
err := fakeClient.Create(context.TODO(), secret) err := fakeClient.Create(context.TODO(), secret)
require.NoError(t, err) require.NoError(t, err)
got, err := baseReconcileLoop.validateCustomization(customization, "spec.groovyScripts") got, err := baseReconcileLoop.validateCustomization(customization, "spec.groovyScripts")
assert.NoError(t, err) assert.NoError(t, err)
assert.False(t, got)
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) { t.Run("secret and configmap exists", func(t *testing.T) {
customization := v1alpha2.Customization{ customization := v1alpha2.Customization{
@ -752,7 +766,7 @@ func TestValidateCustomization(t *testing.T) {
} }
fakeClient := fake.NewFakeClient() fakeClient := fake.NewFakeClient()
baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false),
jenkins, false, false, nil, nil) jenkins, false, false, nil, nil, nil)
err := fakeClient.Create(context.TODO(), secret) err := fakeClient.Create(context.TODO(), secret)
require.NoError(t, err) require.NoError(t, err)
err = fakeClient.Create(context.TODO(), configMap) err = fakeClient.Create(context.TODO(), configMap)
@ -761,7 +775,7 @@ func TestValidateCustomization(t *testing.T) {
got, err := baseReconcileLoop.validateCustomization(customization, "spec.groovyScripts") got, err := baseReconcileLoop.validateCustomization(customization, "spec.groovyScripts")
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, got) assert.Nil(t, got)
}) })
t.Run("secret not exists and configmap exists", func(t *testing.T) { t.Run("secret not exists and configmap exists", func(t *testing.T) {
configMapName := "configmap-name" configMapName := "configmap-name"
@ -777,14 +791,15 @@ func TestValidateCustomization(t *testing.T) {
} }
fakeClient := fake.NewFakeClient() fakeClient := fake.NewFakeClient()
baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false),
jenkins, false, false, nil, nil) jenkins, false, false, nil, nil, nil)
err := fakeClient.Create(context.TODO(), configMap) err := fakeClient.Create(context.TODO(), configMap)
require.NoError(t, err) require.NoError(t, err)
got, err := baseReconcileLoop.validateCustomization(customization, "spec.groovyScripts") got, err := baseReconcileLoop.validateCustomization(customization, "spec.groovyScripts")
assert.NoError(t, err) assert.NoError(t, err)
assert.False(t, got)
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) { t.Run("secret exists and configmap not exists", func(t *testing.T) {
customization := v1alpha2.Customization{ customization := v1alpha2.Customization{
@ -799,13 +814,14 @@ func TestValidateCustomization(t *testing.T) {
} }
fakeClient := fake.NewFakeClient() fakeClient := fake.NewFakeClient()
baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false),
jenkins, false, false, nil, nil) jenkins, false, false, nil, nil, nil)
err := fakeClient.Create(context.TODO(), secret) err := fakeClient.Create(context.TODO(), secret)
require.NoError(t, err) require.NoError(t, err)
got, err := baseReconcileLoop.validateCustomization(customization, "spec.groovyScripts") got, err := baseReconcileLoop.validateCustomization(customization, "spec.groovyScripts")
assert.NoError(t, err) assert.NoError(t, err)
assert.False(t, got)
assert.Equal(t, got, []string{"ConfigMap 'configmap-name' configured in spec.groovyScripts.configurations[0] not found"})
}) })
} }

View File

@ -8,9 +8,6 @@ import (
"strings" "strings"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
"github.com/jenkinsci/kubernetes-operator/pkg/log"
"github.com/go-logr/logr"
stackerr "github.com/pkg/errors" stackerr "github.com/pkg/errors"
"github.com/robfig/cron" "github.com/robfig/cron"
"k8s.io/api/core/v1" "k8s.io/api/core/v1"
@ -19,51 +16,42 @@ import (
) )
// ValidateSeedJobs verify seed jobs configuration // ValidateSeedJobs verify seed jobs configuration
func (s *SeedJobs) ValidateSeedJobs(jenkins v1alpha2.Jenkins) (bool, error) { func (s *SeedJobs) ValidateSeedJobs(jenkins v1alpha2.Jenkins) ([]string, error) {
valid := true var messages []string
if !s.validateIfIDIsUnique(jenkins.Spec.SeedJobs) { if msg := s.validateIfIDIsUnique(jenkins.Spec.SeedJobs); len(msg) > 0 {
valid = false messages = append(messages, msg...)
} }
for _, seedJob := range jenkins.Spec.SeedJobs { for _, seedJob := range jenkins.Spec.SeedJobs {
logger := s.logger.WithValues("seedJob", seedJob.ID).V(log.VWarn)
if len(seedJob.ID) == 0 { if len(seedJob.ID) == 0 {
logger.Info("id can't be empty") messages = append(messages, fmt.Sprintf("seedJob `%s` id can't be empty", seedJob.ID))
valid = false
} }
if len(seedJob.RepositoryBranch) == 0 { if len(seedJob.RepositoryBranch) == 0 {
logger.Info("repository branch can't be empty") messages = append(messages, fmt.Sprintf("seedJob `%s` repository branch can't be empty", seedJob.ID))
valid = false
} }
if len(seedJob.RepositoryURL) == 0 { if len(seedJob.RepositoryURL) == 0 {
logger.Info("repository URL branch can't be empty") messages = append(messages, fmt.Sprintf("seedJob `%s` repository URL branch can't be empty", seedJob.ID))
valid = false
} }
if len(seedJob.Targets) == 0 { if len(seedJob.Targets) == 0 {
logger.Info("targets can't be empty") messages = append(messages, fmt.Sprintf("seedJob `%s` targets can't be empty", seedJob.ID))
valid = false
} }
if _, ok := v1alpha2.AllowedJenkinsCredentialMap[string(seedJob.JenkinsCredentialType)]; !ok { if _, ok := v1alpha2.AllowedJenkinsCredentialMap[string(seedJob.JenkinsCredentialType)]; !ok {
logger.Info("unknown credential type") messages = append(messages, fmt.Sprintf("seedJob `%s` unknown credential type", seedJob.ID))
return false, nil
} }
if (seedJob.JenkinsCredentialType == v1alpha2.BasicSSHCredentialType || if (seedJob.JenkinsCredentialType == v1alpha2.BasicSSHCredentialType ||
seedJob.JenkinsCredentialType == v1alpha2.UsernamePasswordCredentialType) && len(seedJob.CredentialID) == 0 { seedJob.JenkinsCredentialType == v1alpha2.UsernamePasswordCredentialType) && len(seedJob.CredentialID) == 0 {
logger.Info("credential ID can't be empty") messages = append(messages, fmt.Sprintf("seedJob `%s` credential ID can't be empty", seedJob.ID))
valid = false
} }
// validate repository url match private key // validate repository url match private key
if strings.Contains(seedJob.RepositoryURL, "git@") && seedJob.JenkinsCredentialType == v1alpha2.NoJenkinsCredentialCredentialType { if strings.Contains(seedJob.RepositoryURL, "git@") && seedJob.JenkinsCredentialType == v1alpha2.NoJenkinsCredentialCredentialType {
logger.Info("Jenkins credential must be set while using ssh repository url") messages = append(messages, fmt.Sprintf("seedJob `%s` Jenkins credential must be set while using ssh repository url", seedJob.ID))
valid = false
} }
if seedJob.JenkinsCredentialType == v1alpha2.BasicSSHCredentialType || seedJob.JenkinsCredentialType == v1alpha2.UsernamePasswordCredentialType { if seedJob.JenkinsCredentialType == v1alpha2.BasicSSHCredentialType || seedJob.JenkinsCredentialType == v1alpha2.UsernamePasswordCredentialType {
@ -71,56 +59,66 @@ func (s *SeedJobs) ValidateSeedJobs(jenkins v1alpha2.Jenkins) (bool, error) {
namespaceName := types.NamespacedName{Namespace: jenkins.Namespace, Name: seedJob.CredentialID} namespaceName := types.NamespacedName{Namespace: jenkins.Namespace, Name: seedJob.CredentialID}
err := s.k8sClient.Get(context.TODO(), namespaceName, secret) err := s.k8sClient.Get(context.TODO(), namespaceName, secret)
if err != nil && apierrors.IsNotFound(err) { if err != nil && apierrors.IsNotFound(err) {
logger.Info(fmt.Sprintf("required secret '%s' with Jenkins credential not found", seedJob.CredentialID)) messages = append(messages, fmt.Sprintf("seedJob `%s` required secret '%s' with Jenkins credential not found", seedJob.ID, seedJob.CredentialID))
return false, nil
} else if err != nil { } else if err != nil {
return false, stackerr.WithStack(err) return nil, stackerr.WithStack(err)
} }
if seedJob.JenkinsCredentialType == v1alpha2.BasicSSHCredentialType { if seedJob.JenkinsCredentialType == v1alpha2.BasicSSHCredentialType {
if ok := validateBasicSSHSecret(logger, *secret); !ok { if msg := validateBasicSSHSecret(*secret); len(msg) > 0 {
valid = false for _, m := range msg {
messages = append(messages, fmt.Sprintf("seedJob `%s` %s", seedJob.ID, m))
}
} }
} }
if seedJob.JenkinsCredentialType == v1alpha2.UsernamePasswordCredentialType { if seedJob.JenkinsCredentialType == v1alpha2.UsernamePasswordCredentialType {
if ok := validateUsernamePasswordSecret(logger, *secret); !ok { if msg := validateUsernamePasswordSecret(*secret); len(msg) > 0 {
valid = false for _, m := range msg {
messages = append(messages, fmt.Sprintf("seedJob `%s` %s", seedJob.ID, m))
}
} }
} }
} }
if len(seedJob.BuildPeriodically) > 0 { if len(seedJob.BuildPeriodically) > 0 {
if !s.validateSchedule(seedJob, seedJob.BuildPeriodically, "buildPeriodically") { if msg := s.validateSchedule(seedJob, seedJob.BuildPeriodically, "buildPeriodically"); len(msg) > 0 {
valid = false for _, m := range msg {
messages = append(messages, fmt.Sprintf("seedJob `%s` %s", seedJob.ID, m))
}
} }
} }
if len(seedJob.PollSCM) > 0 { if len(seedJob.PollSCM) > 0 {
if !s.validateSchedule(seedJob, seedJob.PollSCM, "pollSCM") { if msg := s.validateSchedule(seedJob, seedJob.PollSCM, "pollSCM"); len(msg) > 0 {
valid = false for _, m := range msg {
messages = append(messages, fmt.Sprintf("seedJob `%s` %s", seedJob.ID, m))
}
} }
} }
if seedJob.GitHubPushTrigger { if seedJob.GitHubPushTrigger {
if !s.validateGitHubPushTrigger(jenkins) { if msg := s.validateGitHubPushTrigger(jenkins); len(msg) > 0 {
valid = false for _, m := range msg {
messages = append(messages, fmt.Sprintf("seedJob `%s` %s", seedJob.ID, m))
}
} }
} }
} }
return valid, nil return messages, nil
} }
func (s *SeedJobs) validateSchedule(job v1alpha2.SeedJob, str string, key string) bool { func (s *SeedJobs) validateSchedule(job v1alpha2.SeedJob, str string, key string) []string {
var messages []string
_, err := cron.Parse(str) _, err := cron.Parse(str)
if err != nil { if err != nil {
s.logger.V(log.VWarn).Info(fmt.Sprintf("`%s` schedule '%s' is invalid cron spec in `%s`", key, str, job.ID)) messages = append(messages, fmt.Sprintf("`%s` schedule '%s' is invalid cron spec in `%s`", key, str, job.ID))
return false
} }
return true return messages
} }
func (s *SeedJobs) validateGitHubPushTrigger(jenkins v1alpha2.Jenkins) bool { func (s *SeedJobs) validateGitHubPushTrigger(jenkins v1alpha2.Jenkins) []string {
var messages []string
exists := false exists := false
for _, plugin := range jenkins.Spec.Master.BasePlugins { for _, plugin := range jenkins.Spec.Master.BasePlugins {
if plugin.Name == "github" { if plugin.Name == "github" {
@ -136,75 +134,65 @@ func (s *SeedJobs) validateGitHubPushTrigger(jenkins v1alpha2.Jenkins) bool {
} }
if !exists && !userExists { if !exists && !userExists {
s.logger.V(log.VWarn).Info("githubPushTrigger is set. This function requires `github` plugin installed in .Spec.Master.Plugins because seed jobs Push Trigger function needs it") messages = append(messages, "githubPushTrigger is set. This function requires `github` plugin installed in .Spec.Master.Plugins because seed jobs Push Trigger function needs it")
return false
} }
return true return messages
} }
func (s *SeedJobs) validateIfIDIsUnique(seedJobs []v1alpha2.SeedJob) bool { func (s *SeedJobs) validateIfIDIsUnique(seedJobs []v1alpha2.SeedJob) []string {
var messages []string
ids := map[string]bool{} ids := map[string]bool{}
for _, seedJob := range seedJobs { for _, seedJob := range seedJobs {
if _, found := ids[seedJob.ID]; found { if _, found := ids[seedJob.ID]; found {
s.logger.V(log.VWarn).Info(fmt.Sprintf("'%s' seed job ID is not unique", seedJob.ID)) messages = append(messages, fmt.Sprintf("'%s' seed job ID is not unique", seedJob.ID))
return false
} }
ids[seedJob.ID] = true ids[seedJob.ID] = true
} }
return true return messages
} }
func validateBasicSSHSecret(logger logr.InfoLogger, secret v1.Secret) bool { func validateBasicSSHSecret(secret v1.Secret) []string {
valid := true var messages []string
username, exists := secret.Data[UsernameSecretKey] username, exists := secret.Data[UsernameSecretKey]
if !exists { if !exists {
logger.Info(fmt.Sprintf("required data '%s' not found in secret '%s'", UsernameSecretKey, secret.ObjectMeta.Name)) messages = append(messages, fmt.Sprintf("required data '%s' not found in secret '%s'", UsernameSecretKey, secret.ObjectMeta.Name))
valid = false
} }
if len(username) == 0 { if len(username) == 0 {
logger.Info(fmt.Sprintf("required data '%s' is empty in secret '%s'", UsernameSecretKey, secret.ObjectMeta.Name)) messages = append(messages, fmt.Sprintf("required data '%s' is empty in secret '%s'", UsernameSecretKey, secret.ObjectMeta.Name))
valid = false
} }
privateKey, exists := secret.Data[PrivateKeySecretKey] privateKey, exists := secret.Data[PrivateKeySecretKey]
if !exists { if !exists {
logger.Info(fmt.Sprintf("required data '%s' not found in secret '%s'", PrivateKeySecretKey, secret.ObjectMeta.Name)) messages = append(messages, fmt.Sprintf("required data '%s' not found in secret '%s'", PrivateKeySecretKey, secret.ObjectMeta.Name))
valid = false
} }
if len(string(privateKey)) == 0 { if len(string(privateKey)) == 0 {
logger.Info(fmt.Sprintf("required data '%s' not found in secret '%s'", PrivateKeySecretKey, secret.ObjectMeta.Name)) messages = append(messages, fmt.Sprintf("required data '%s' not found in secret '%s'", PrivateKeySecretKey, secret.ObjectMeta.Name))
return false
} }
if err := validatePrivateKey(string(privateKey)); err != nil { if err := validatePrivateKey(string(privateKey)); err != nil {
logger.Info(fmt.Sprintf("private key '%s' invalid in secret '%s': %s", PrivateKeySecretKey, secret.ObjectMeta.Name, err)) messages = append(messages, fmt.Sprintf("private key '%s' invalid in secret '%s': %s", PrivateKeySecretKey, secret.ObjectMeta.Name, err))
valid = false
} }
return valid return messages
} }
func validateUsernamePasswordSecret(logger logr.InfoLogger, secret v1.Secret) bool { func validateUsernamePasswordSecret(secret v1.Secret) []string {
valid := true var messages []string
username, exists := secret.Data[UsernameSecretKey] username, exists := secret.Data[UsernameSecretKey]
if !exists { if !exists {
logger.Info(fmt.Sprintf("required data '%s' not found in secret '%s'", UsernameSecretKey, secret.ObjectMeta.Name)) messages = append(messages, fmt.Sprintf("required data '%s' not found in secret '%s'", UsernameSecretKey, secret.ObjectMeta.Name))
valid = false
} }
if len(username) == 0 { if len(username) == 0 {
logger.Info(fmt.Sprintf("required data '%s' is empty in secret '%s'", UsernameSecretKey, secret.ObjectMeta.Name)) messages = append(messages, fmt.Sprintf("required data '%s' is empty in secret '%s'", UsernameSecretKey, secret.ObjectMeta.Name))
valid = false
} }
password, exists := secret.Data[PasswordSecretKey] password, exists := secret.Data[PasswordSecretKey]
if !exists { if !exists {
logger.Info(fmt.Sprintf("required data '%s' not found in secret '%s'", PasswordSecretKey, secret.ObjectMeta.Name)) messages = append(messages, fmt.Sprintf("required data '%s' not found in secret '%s'", PasswordSecretKey, secret.ObjectMeta.Name))
valid = false
} }
if len(password) == 0 { if len(password) == 0 {
logger.Info(fmt.Sprintf("required data '%s' is empty in secret '%s'", PasswordSecretKey, secret.ObjectMeta.Name)) messages = append(messages, fmt.Sprintf("required data '%s' is empty in secret '%s'", PasswordSecretKey, secret.ObjectMeta.Name))
valid = false
} }
return valid return messages
} }
func validatePrivateKey(privateKey string) error { func validatePrivateKey(privateKey string) error {

View File

@ -76,7 +76,7 @@ func TestValidateSeedJobs(t *testing.T) {
result, err := seedJobs.ValidateSeedJobs(jenkins) result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, true, result) assert.Nil(t, result)
}) })
t.Run("Invalid without id", func(t *testing.T) { t.Run("Invalid without id", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{ jenkins := v1alpha2.Jenkins{
@ -96,7 +96,8 @@ func TestValidateSeedJobs(t *testing.T) {
result, err := seedJobs.ValidateSeedJobs(jenkins) result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, false, result)
assert.Equal(t, result, []string{"seedJob `` id can't be empty"})
}) })
t.Run("Valid with private key and secret", func(t *testing.T) { t.Run("Valid with private key and secret", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{ jenkins := v1alpha2.Jenkins{
@ -129,7 +130,7 @@ func TestValidateSeedJobs(t *testing.T) {
result, err := seedJobs.ValidateSeedJobs(jenkins) result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, true, result) assert.Nil(t, result)
}) })
t.Run("Invalid private key in secret", func(t *testing.T) { t.Run("Invalid private key in secret", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{ jenkins := v1alpha2.Jenkins{
@ -162,7 +163,8 @@ func TestValidateSeedJobs(t *testing.T) {
result, err := seedJobs.ValidateSeedJobs(jenkins) result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, false, result)
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) { t.Run("Invalid with PrivateKey and empty Secret data", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{ jenkins := v1alpha2.Jenkins{
@ -195,7 +197,8 @@ func TestValidateSeedJobs(t *testing.T) {
result, err := seedJobs.ValidateSeedJobs(jenkins) result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, false, result)
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) { t.Run("Invalid with ssh RepositoryURL and empty PrivateKey", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{ jenkins := v1alpha2.Jenkins{
@ -217,7 +220,8 @@ func TestValidateSeedJobs(t *testing.T) {
result, err := seedJobs.ValidateSeedJobs(jenkins) result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, false, result)
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) { t.Run("Invalid without targets", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{ jenkins := v1alpha2.Jenkins{
@ -237,7 +241,8 @@ func TestValidateSeedJobs(t *testing.T) {
result, err := seedJobs.ValidateSeedJobs(jenkins) result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, false, result)
assert.Equal(t, result, []string{"seedJob `example` targets can't be empty"})
}) })
t.Run("Invalid without repository URL", func(t *testing.T) { t.Run("Invalid without repository URL", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{ jenkins := v1alpha2.Jenkins{
@ -257,7 +262,8 @@ func TestValidateSeedJobs(t *testing.T) {
result, err := seedJobs.ValidateSeedJobs(jenkins) result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, false, result)
assert.Equal(t, result, []string{"seedJob `example` repository URL branch can't be empty"})
}) })
t.Run("Invalid without repository branch", func(t *testing.T) { t.Run("Invalid without repository branch", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{ jenkins := v1alpha2.Jenkins{
@ -277,7 +283,8 @@ func TestValidateSeedJobs(t *testing.T) {
result, err := seedJobs.ValidateSeedJobs(jenkins) result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, false, result)
assert.Equal(t, result, []string{"seedJob `example` repository branch can't be empty"})
}) })
t.Run("Valid with username and password", func(t *testing.T) { t.Run("Valid with username and password", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{ jenkins := v1alpha2.Jenkins{
@ -310,7 +317,7 @@ func TestValidateSeedJobs(t *testing.T) {
result, err := seedJobs.ValidateSeedJobs(jenkins) result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, true, result) assert.Nil(t, result)
}) })
t.Run("Invalid with empty username", func(t *testing.T) { t.Run("Invalid with empty username", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{ jenkins := v1alpha2.Jenkins{
@ -343,7 +350,8 @@ func TestValidateSeedJobs(t *testing.T) {
result, err := seedJobs.ValidateSeedJobs(jenkins) result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, false, result)
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) { t.Run("Invalid with empty password", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{ jenkins := v1alpha2.Jenkins{
@ -376,7 +384,8 @@ func TestValidateSeedJobs(t *testing.T) {
result, err := seedJobs.ValidateSeedJobs(jenkins) result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, false, result)
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) { t.Run("Invalid without username", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{ jenkins := v1alpha2.Jenkins{
@ -408,7 +417,8 @@ func TestValidateSeedJobs(t *testing.T) {
result, err := seedJobs.ValidateSeedJobs(jenkins) result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, false, result)
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) { t.Run("Invalid without password", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{ jenkins := v1alpha2.Jenkins{
@ -440,7 +450,8 @@ func TestValidateSeedJobs(t *testing.T) {
result, err := seedJobs.ValidateSeedJobs(jenkins) result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, false, result)
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) { t.Run("Invalid with wrong cron spec", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{ jenkins := v1alpha2.Jenkins{
@ -463,7 +474,8 @@ func TestValidateSeedJobs(t *testing.T) {
result, err := seedJobs.ValidateSeedJobs(jenkins) result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err) assert.NoError(t, err)
assert.False(t, result)
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) { t.Run("Valid with good cron spec", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{ jenkins := v1alpha2.Jenkins{
@ -487,7 +499,7 @@ func TestValidateSeedJobs(t *testing.T) {
result, err := seedJobs.ValidateSeedJobs(jenkins) result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, result) assert.Nil(t, result)
}) })
t.Run("Invalid with set githubPushTrigger and not installed github plugin", func(t *testing.T) { t.Run("Invalid with set githubPushTrigger and not installed github plugin", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{ jenkins := v1alpha2.Jenkins{
@ -510,7 +522,8 @@ func TestValidateSeedJobs(t *testing.T) {
result, err := seedJobs.ValidateSeedJobs(jenkins) result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err) assert.NoError(t, err)
assert.False(t, result)
assert.Equal(t, result, []string{"seedJob `example` githubPushTrigger is set. This function requires `github` plugin installed in .Spec.Master.Plugins because seed jobs Push Trigger function needs it"})
}) })
t.Run("Invalid with set githubPushTrigger and not installed github plugin", func(t *testing.T) { t.Run("Invalid with set githubPushTrigger and not installed github plugin", func(t *testing.T) {
jenkins := v1alpha2.Jenkins{ jenkins := v1alpha2.Jenkins{
@ -538,7 +551,7 @@ func TestValidateSeedJobs(t *testing.T) {
result, err := seedJobs.ValidateSeedJobs(jenkins) result, err := seedJobs.ValidateSeedJobs(jenkins)
assert.NoError(t, err) assert.NoError(t, err)
assert.True(t, result) assert.Nil(t, result)
}) })
} }
@ -549,7 +562,7 @@ func TestValidateIfIDIsUnique(t *testing.T) {
} }
ctrl := New(nil, nil, logf.ZapLogger(false)) ctrl := New(nil, nil, logf.ZapLogger(false))
got := ctrl.validateIfIDIsUnique(seedJobs) got := ctrl.validateIfIDIsUnique(seedJobs)
assert.Equal(t, true, got) assert.Nil(t, got)
}) })
t.Run("duplicated ids", func(t *testing.T) { t.Run("duplicated ids", func(t *testing.T) {
seedJobs := []v1alpha2.SeedJob{ seedJobs := []v1alpha2.SeedJob{
@ -557,6 +570,7 @@ func TestValidateIfIDIsUnique(t *testing.T) {
} }
ctrl := New(nil, nil, logf.ZapLogger(false)) ctrl := New(nil, nil, logf.ZapLogger(false))
got := ctrl.validateIfIDIsUnique(seedJobs) got := ctrl.validateIfIDIsUnique(seedJobs)
assert.Equal(t, false, got)
assert.Equal(t, got, []string{"'first' seed job ID is not unique"})
}) })
} }

View File

@ -7,10 +7,10 @@ import (
) )
// Validate validates Jenkins CR Spec section // Validate validates Jenkins CR Spec section
func (r *ReconcileUserConfiguration) Validate(jenkins *v1alpha2.Jenkins) (bool, error) { func (r *ReconcileUserConfiguration) Validate(jenkins *v1alpha2.Jenkins) ([]string, error) {
backupAndRestore := backuprestore.New(r.k8sClient, r.clientSet, r.logger, r.jenkins, r.config) backupAndRestore := backuprestore.New(r.k8sClient, r.clientSet, r.logger, r.jenkins, r.config)
if ok := backupAndRestore.Validate(); !ok { if msg := backupAndRestore.Validate(); msg != nil {
return false, nil return msg, nil
} }
seedJobs := seedjobs.New(r.jenkinsClient, r.k8sClient, r.logger) seedJobs := seedjobs.New(r.jenkinsClient, r.k8sClient, r.logger)

View File

@ -49,7 +49,11 @@ func (g *Groovy) EnsureSingle(source, name, hash, groovyScript string) (requeue
logs, err := g.jenkinsClient.ExecuteScript(groovyScript) logs, err := g.jenkinsClient.ExecuteScript(groovyScript)
if err != nil { if err != nil {
if _, ok := err.(*jenkinsclient.GroovyScriptExecutionFailed); ok { 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)) 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 return true, err

View File

@ -11,8 +11,8 @@ import (
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources" "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/configuration/user"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/notifications"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/plugins" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/plugins"
"github.com/jenkinsci/kubernetes-operator/pkg/event"
"github.com/jenkinsci/kubernetes-operator/pkg/log" "github.com/jenkinsci/kubernetes-operator/pkg/log"
"github.com/jenkinsci/kubernetes-operator/version" "github.com/jenkinsci/kubernetes-operator/version"
@ -34,15 +34,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/source" "sigs.k8s.io/controller-runtime/pkg/source"
) )
const (
// reasonBaseConfigurationSuccess is the event which informs base configuration has been completed successfully
reasonBaseConfigurationSuccess event.Reason = "BaseConfigurationSuccess"
// reasonUserConfigurationSuccess is the event which informs user configuration has been completed successfully
reasonUserConfigurationSuccess event.Reason = "BaseConfigurationFailure"
// reasonCRValidationFailure is the event which informs user has provided invalid configuration in Jenkins CR
reasonCRValidationFailure event.Reason = "CRValidationFailure"
)
type reconcileError struct { type reconcileError struct {
err error err error
counter uint64 counter uint64
@ -52,20 +43,20 @@ var reconcileErrors = map[string]reconcileError{}
// Add creates a new Jenkins Controller and adds it to the Manager. The Manager will set fields on the Controller // Add creates a new Jenkins Controller and adds it to the Manager. The Manager will set fields on the Controller
// and Start it when the Manager is Started. // and Start it when the Manager is Started.
func Add(mgr manager.Manager, local, minikube bool, events event.Recorder, clientSet kubernetes.Clientset, config rest.Config) error { func Add(mgr manager.Manager, local, minikube bool, clientSet kubernetes.Clientset, config rest.Config, notificationEvents *chan notifications.Event) error {
return add(mgr, newReconciler(mgr, local, minikube, events, clientSet, config)) return add(mgr, newReconciler(mgr, local, minikube, clientSet, config, notificationEvents))
} }
// newReconciler returns a new reconcile.Reconciler // newReconciler returns a new reconcile.Reconciler
func newReconciler(mgr manager.Manager, local, minikube bool, events event.Recorder, clientSet kubernetes.Clientset, config rest.Config) reconcile.Reconciler { func newReconciler(mgr manager.Manager, local, minikube bool, clientSet kubernetes.Clientset, config rest.Config, notificationEvents *chan notifications.Event) reconcile.Reconciler {
return &ReconcileJenkins{ return &ReconcileJenkins{
client: mgr.GetClient(), client: mgr.GetClient(),
scheme: mgr.GetScheme(), scheme: mgr.GetScheme(),
local: local, local: local,
minikube: minikube, minikube: minikube,
events: events,
clientSet: clientSet, clientSet: clientSet,
config: config, config: config,
notificationEvents: notificationEvents,
} }
} }
@ -122,17 +113,18 @@ type ReconcileJenkins struct {
client client.Client client client.Client
scheme *runtime.Scheme scheme *runtime.Scheme
local, minikube bool local, minikube bool
events event.Recorder
clientSet kubernetes.Clientset clientSet kubernetes.Clientset
config rest.Config config rest.Config
notificationEvents *chan notifications.Event
} }
// Reconcile it's a main reconciliation loop which maintain desired state based on Jenkins.Spec // Reconcile it's a main reconciliation loop which maintain desired state based on Jenkins.Spec
func (r *ReconcileJenkins) Reconcile(request reconcile.Request) (reconcile.Result, error) { func (r *ReconcileJenkins) Reconcile(request reconcile.Request) (reconcile.Result, error) {
reconcileFailLimit := uint64(10)
logger := r.buildLogger(request.Name) logger := r.buildLogger(request.Name)
logger.V(log.VDebug).Info("Reconciling Jenkins") logger.V(log.VDebug).Info("Reconciling Jenkins")
result, err := r.reconcile(request, logger) result, jenkins, err := r.reconcile(request, logger)
if err != nil && apierrors.IsConflict(err) { if err != nil && apierrors.IsConflict(err) {
return reconcile.Result{Requeue: true}, nil return reconcile.Result{Requeue: true}, nil
} else if err != nil { } else if err != nil {
@ -151,11 +143,19 @@ func (r *ReconcileJenkins) Reconcile(request reconcile.Request) (reconcile.Resul
} }
} }
reconcileErrors[request.Name] = lastErrors reconcileErrors[request.Name] = lastErrors
if lastErrors.counter >= 15 { if lastErrors.counter >= reconcileFailLimit {
if log.Debug { if log.Debug {
logger.V(log.VWarn).Info(fmt.Sprintf("Reconcile loop failed ten times with the same error, giving up: %+v", err)) logger.V(log.VWarn).Info(fmt.Sprintf("Reconcile loop failed %d times with the same error, giving up: %+v", reconcileFailLimit, err))
} else { } else {
logger.V(log.VWarn).Info(fmt.Sprintf("Reconcile loop failed ten times with the same error, giving up: %s", err)) logger.V(log.VWarn).Info(fmt.Sprintf("Reconcile loop failed %d times with the same error, giving up: %s", reconcileFailLimit, err))
}
*r.notificationEvents <- notifications.Event{
Jenkins: *jenkins,
Phase: notifications.PhaseBase,
LogLevel: v1alpha2.NotificationLogLevelWarning,
Message: fmt.Sprintf("Reconcile loop failed %d times with the same error, giving up: %s", reconcileFailLimit, err),
MessagesVerbose: []string{fmt.Sprintf("Reconcile loop failed %d times with the same error, giving up: %+v", reconcileFailLimit, err)},
} }
return reconcile.Result{Requeue: false}, nil return reconcile.Result{Requeue: false}, nil
} }
@ -168,7 +168,14 @@ func (r *ReconcileJenkins) Reconcile(request reconcile.Request) (reconcile.Resul
} }
} }
if _, ok := err.(*jenkinsclient.GroovyScriptExecutionFailed); ok { if groovyErr, ok := err.(*jenkinsclient.GroovyScriptExecutionFailed); ok {
*r.notificationEvents <- notifications.Event{
Jenkins: *jenkins,
Phase: notifications.PhaseBase,
LogLevel: v1alpha2.NotificationLogLevelWarning,
Message: fmt.Sprintf("%s Source '%s' Name '%s' groovy script execution failed, logs:", groovyErr.ConfigurationType, groovyErr.Source, groovyErr.Name),
MessagesVerbose: []string{groovyErr.Logs},
}
return reconcile.Result{Requeue: false}, nil return reconcile.Result{Requeue: false}, nil
} }
return reconcile.Result{Requeue: true}, nil return reconcile.Result{Requeue: true}, nil
@ -176,7 +183,7 @@ func (r *ReconcileJenkins) Reconcile(request reconcile.Request) (reconcile.Resul
return result, nil return result, nil
} }
func (r *ReconcileJenkins) reconcile(request reconcile.Request, logger logr.Logger) (reconcile.Result, error) { func (r *ReconcileJenkins) reconcile(request reconcile.Request, logger logr.Logger) (reconcile.Result, *v1alpha2.Jenkins, error) {
// Fetch the Jenkins instance // Fetch the Jenkins instance
jenkins := &v1alpha2.Jenkins{} jenkins := &v1alpha2.Jenkins{}
err := r.client.Get(context.TODO(), request.NamespacedName, jenkins) err := r.client.Get(context.TODO(), request.NamespacedName, jenkins)
@ -185,38 +192,47 @@ func (r *ReconcileJenkins) reconcile(request reconcile.Request, logger logr.Logg
// Request object not found, could have been deleted after reconcile request. // Request object not found, could have been deleted after reconcile request.
// Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers.
// Return and don't requeue // Return and don't requeue
return reconcile.Result{}, nil return reconcile.Result{}, nil, nil
} }
// Error reading the object - requeue the request. // Error reading the object - requeue the request.
return reconcile.Result{}, errors.WithStack(err) return reconcile.Result{}, nil, errors.WithStack(err)
} }
err = r.setDefaults(jenkins, logger) err = r.setDefaults(jenkins, logger)
if err != nil { if err != nil {
return reconcile.Result{}, err return reconcile.Result{}, jenkins, err
} }
// Reconcile base configuration // Reconcile base configuration
baseConfiguration := base.New(r.client, r.scheme, logger, jenkins, r.local, r.minikube, &r.clientSet, &r.config) baseConfiguration := base.New(r.client, r.scheme, logger, jenkins, r.local, r.minikube, &r.clientSet, &r.config, r.notificationEvents)
valid, err := baseConfiguration.Validate(jenkins) messages, err := baseConfiguration.Validate(jenkins)
if err != nil { if err != nil {
return reconcile.Result{}, err return reconcile.Result{}, jenkins, err
} }
if !valid { if len(messages) > 0 {
r.events.Emit(jenkins, event.TypeWarning, reasonCRValidationFailure, "Base CR validation failed") message := "Validation of base configuration failed, please correct Jenkins CR."
logger.V(log.VWarn).Info("Validation of base configuration failed, please correct Jenkins CR") *r.notificationEvents <- notifications.Event{
return reconcile.Result{}, nil // don't requeue Jenkins: *jenkins,
Phase: notifications.PhaseBase,
LogLevel: v1alpha2.NotificationLogLevelWarning,
Message: message,
MessagesVerbose: messages,
}
logger.V(log.VWarn).Info(message)
for _, msg := range messages {
logger.V(log.VWarn).Info(msg)
}
return reconcile.Result{}, jenkins, nil // don't requeue
} }
result, jenkinsClient, err := baseConfiguration.Reconcile() result, jenkinsClient, err := baseConfiguration.Reconcile()
if err != nil { if err != nil {
return reconcile.Result{}, err return reconcile.Result{}, jenkins, err
} }
if result.Requeue { if result.Requeue {
return result, nil return result, jenkins, nil
} }
if jenkinsClient == nil { if jenkinsClient == nil {
return reconcile.Result{Requeue: false}, nil return reconcile.Result{Requeue: false}, jenkins, nil
} }
if jenkins.Status.BaseConfigurationCompletedTime == nil { if jenkins.Status.BaseConfigurationCompletedTime == nil {
@ -224,31 +240,50 @@ func (r *ReconcileJenkins) reconcile(request reconcile.Request, logger logr.Logg
jenkins.Status.BaseConfigurationCompletedTime = &now jenkins.Status.BaseConfigurationCompletedTime = &now
err = r.client.Update(context.TODO(), jenkins) err = r.client.Update(context.TODO(), jenkins)
if err != nil { if err != nil {
return reconcile.Result{}, errors.WithStack(err) return reconcile.Result{}, jenkins, errors.WithStack(err)
} }
logger.Info(fmt.Sprintf("Base configuration phase is complete, took %s",
jenkins.Status.BaseConfigurationCompletedTime.Sub(jenkins.Status.ProvisionStartTime.Time))) message := fmt.Sprintf("Base configuration phase is complete, took %s",
r.events.Emit(jenkins, event.TypeNormal, reasonBaseConfigurationSuccess, "Base configuration completed") jenkins.Status.BaseConfigurationCompletedTime.Sub(jenkins.Status.ProvisionStartTime.Time))
*r.notificationEvents <- notifications.Event{
Jenkins: *jenkins,
Phase: notifications.PhaseBase,
LogLevel: v1alpha2.NotificationLogLevelInfo,
Message: message,
MessagesVerbose: messages,
}
logger.Info(message)
} }
// Reconcile user configuration // Reconcile user configuration
userConfiguration := user.New(r.client, jenkinsClient, logger, jenkins, r.clientSet, r.config) userConfiguration := user.New(r.client, jenkinsClient, logger, jenkins, r.clientSet, r.config)
valid, err = userConfiguration.Validate(jenkins) messages, err = userConfiguration.Validate(jenkins)
if err != nil { if err != nil {
return reconcile.Result{}, err return reconcile.Result{}, jenkins, err
} }
if !valid { if len(messages) > 0 {
logger.V(log.VWarn).Info("Validation of user configuration failed, please correct Jenkins CR") message := fmt.Sprintf("Validation of user configuration failed, please correct Jenkins CR")
r.events.Emit(jenkins, event.TypeWarning, reasonCRValidationFailure, "User CR validation failed") *r.notificationEvents <- notifications.Event{
return reconcile.Result{}, nil // don't requeue Jenkins: *jenkins,
Phase: notifications.PhaseUser,
LogLevel: v1alpha2.NotificationLogLevelWarning,
Message: message,
MessagesVerbose: messages,
}
logger.V(log.VWarn).Info(message)
for _, msg := range messages {
logger.V(log.VWarn).Info(msg)
}
return reconcile.Result{}, jenkins, nil // don't requeue
} }
result, err = userConfiguration.Reconcile() result, err = userConfiguration.Reconcile()
if err != nil { if err != nil {
return reconcile.Result{}, err return reconcile.Result{}, jenkins, err
} }
if result.Requeue { if result.Requeue {
return result, nil return result, jenkins, nil
} }
if jenkins.Status.UserConfigurationCompletedTime == nil { if jenkins.Status.UserConfigurationCompletedTime == nil {
@ -256,14 +291,21 @@ func (r *ReconcileJenkins) reconcile(request reconcile.Request, logger logr.Logg
jenkins.Status.UserConfigurationCompletedTime = &now jenkins.Status.UserConfigurationCompletedTime = &now
err = r.client.Update(context.TODO(), jenkins) err = r.client.Update(context.TODO(), jenkins)
if err != nil { if err != nil {
return reconcile.Result{}, errors.WithStack(err) return reconcile.Result{}, jenkins, errors.WithStack(err)
} }
logger.Info(fmt.Sprintf("User configuration phase is complete, took %s", message := fmt.Sprintf("User configuration phase is complete, took %s",
jenkins.Status.UserConfigurationCompletedTime.Sub(jenkins.Status.ProvisionStartTime.Time))) jenkins.Status.UserConfigurationCompletedTime.Sub(jenkins.Status.ProvisionStartTime.Time))
r.events.Emit(jenkins, event.TypeNormal, reasonUserConfigurationSuccess, "User configuration completed") *r.notificationEvents <- notifications.Event{
Jenkins: *jenkins,
Phase: notifications.PhaseUser,
LogLevel: v1alpha2.NotificationLogLevelInfo,
Message: message,
MessagesVerbose: messages,
}
logger.Info(message)
} }
return reconcile.Result{}, nil return reconcile.Result{}, jenkins, nil
} }
func (r *ReconcileJenkins) buildLogger(jenkinsName string) logr.Logger { func (r *ReconcileJenkins) buildLogger(jenkinsName string) logr.Logger {

View File

@ -20,21 +20,17 @@ const content = `
<html> <html>
<head></head> <head></head>
<body> <body>
<h1 style="background-color: %s; color: white; padding: 3px 10px;">Jenkins Operator Reconciled</h1> <h1 style="background-color: %s; color: white; padding: 3px 10px;">%s</h1>
<h3>Failed to do something</h3> <h3>%s</h3>
<table> <table>
<tr> <tr>
<td><b>CR name:</b></td> <td><b>CR name:</b></td>
<td>%s</td> <td>%s</td>
</tr> </tr>
<tr> <tr>
<td><b>Configuration type:</b></td> <td><b>Phase:</b></td>
<td>%s</td> <td>%s</td>
</tr> </tr>
<tr>
<td><b>Status:</b></td>
<td><b style="color: %s;">%s</b></td>
</tr>
</table> </table>
<h6 style="font-size: 11px; color: grey; margin-top: 15px;">Powered by Jenkins Operator <3</h6> <h6 style="font-size: 11px; color: grey; margin-top: 15px;">Powered by Jenkins Operator <3</h6>
</body> </body>
@ -69,14 +65,27 @@ func (m MailGun) Send(event Event, config v1alpha2.Notification) error {
secretValue := string(secret.Data[selector.Key]) secretValue := string(secret.Data[selector.Key])
if secretValue == "" { if secretValue == "" {
return errors.Errorf("Mailgun API is empty in secret '%s/%s[%s]", event.Jenkins.Namespace, selector.Name, selector.Key) return errors.Errorf("Mailgun API secret is empty in secret '%s/%s[%s]", event.Jenkins.Namespace, selector.Name, selector.Key)
} }
mg := mailgun.NewMailgun(config.Mailgun.Domain, secretValue) mg := mailgun.NewMailgun(config.Mailgun.Domain, secretValue)
htmlMessage := fmt.Sprintf(content, m.getStatusColor(event.LogLevel), event.Jenkins.Name, event.ConfigurationType, m.getStatusColor(event.LogLevel), string(event.LogLevel)) var statusMessage string
msg := mg.NewMessage(fmt.Sprintf("Jenkins Operator Notifier <%s>", config.Mailgun.From), "Jenkins Operator Status", "", config.Mailgun.Recipient) if config.Verbose {
message := event.Message + "<ul>"
for _, msg := range event.MessagesVerbose {
message = message + "<li>" + msg + "</li>"
}
message = message + "</ul>"
statusMessage = message
} else {
statusMessage = event.Message
}
htmlMessage := fmt.Sprintf(content, m.getStatusColor(event.LogLevel), notificationTitle(event), statusMessage, event.Jenkins.Name, event.Phase)
msg := mg.NewMessage(fmt.Sprintf("Jenkins Operator Notifier <%s>", config.Mailgun.From), notificationTitle(event), "", config.Mailgun.Recipient)
msg.SetHtml(htmlMessage) msg.SetHtml(htmlMessage)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel() defer cancel()

View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
@ -26,6 +27,7 @@ type TeamsMessage struct {
ThemeColor StatusColor `json:"themeColor"` ThemeColor StatusColor `json:"themeColor"`
Title string `json:"title"` Title string `json:"title"`
Sections []TeamsSection `json:"sections"` Sections []TeamsSection `json:"sections"`
Summary string `json:"summary"`
} }
// TeamsSection is MS Teams message section // TeamsSection is MS Teams message section
@ -64,14 +66,13 @@ func (t Teams) Send(event Event, config v1alpha2.Notification) error {
secretValue := string(secret.Data[selector.Key]) secretValue := string(secret.Data[selector.Key])
if secretValue == "" { if secretValue == "" {
return errors.Errorf("Microsoft Teams webhook URL is empty in secret '%s/%s[%s]", event.Jenkins.Namespace, selector.Name, selector.Key) return errors.Errorf("Microsoft Teams WebHook URL is empty in secret '%s/%s[%s]", event.Jenkins.Namespace, selector.Name, selector.Key)
} }
msg, err := json.Marshal(TeamsMessage{ tm := &TeamsMessage{
Type: "MessageCard", Type: "MessageCard",
Context: "https://schema.org/extensions", Context: "https://schema.org/extensions",
ThemeColor: t.getStatusColor(event.LogLevel), ThemeColor: t.getStatusColor(event.LogLevel),
Title: titleText,
Sections: []TeamsSection{ Sections: []TeamsSection{
{ {
Facts: []TeamsFact{ Facts: []TeamsFact{
@ -79,14 +80,6 @@ func (t Teams) Send(event Event, config v1alpha2.Notification) error {
Name: crNameFieldName, Name: crNameFieldName,
Value: event.Jenkins.Name, Value: event.Jenkins.Name,
}, },
{
Name: configurationTypeFieldName,
Value: event.ConfigurationType,
},
{
Name: loggingLevelFieldName,
Value: string(event.LogLevel),
},
{ {
Name: namespaceFieldName, Name: namespaceFieldName,
Value: event.Jenkins.Namespace, Value: event.Jenkins.Namespace,
@ -95,7 +88,28 @@ func (t Teams) Send(event Event, config v1alpha2.Notification) error {
Text: event.Message, Text: event.Message,
}, },
}, },
Summary: event.Message,
}
tm.Title = notificationTitle(event)
if config.Verbose {
message := event.Message
for _, msg := range event.MessagesVerbose {
message = message + "\n\n - " + msg
}
tm.Sections[0].Text += message
tm.Summary = message
}
if event.Phase != PhaseUnknown {
tm.Sections[0].Facts = append(tm.Sections[0].Facts, TeamsFact{
Name: phaseFieldName,
Value: string(event.Phase),
}) })
}
msg, err := json.Marshal(tm)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
@ -110,6 +124,9 @@ func (t Teams) Send(event Event, config v1alpha2.Notification) error {
return errors.WithStack(err) 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() }() defer func() { _ = resp.Body.Close() }()
return nil return nil

View File

@ -27,9 +27,9 @@ func TestTeams_Send(t *testing.T) {
Namespace: testNamespace, Namespace: testNamespace,
}, },
}, },
ConfigurationType: testConfigurationType, Phase: testPhase,
Message: testMessage, Message: testMessage,
MessageVerbose: testMessageVerbose, MessagesVerbose: testMessageVerbose,
LogLevel: testLoggingLevel, LogLevel: testLoggingLevel,
} }
teams := Teams{k8sClient: fakeClient} teams := Teams{k8sClient: fakeClient}
@ -42,7 +42,7 @@ func TestTeams_Send(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
assert.Equal(t, message.Title, titleText) assert.Equal(t, message.Title, notificationTitle(event))
assert.Equal(t, message.ThemeColor, teams.getStatusColor(event.LogLevel)) assert.Equal(t, message.ThemeColor, teams.getStatusColor(event.LogLevel))
mainSection := message.Sections[0] mainSection := message.Sections[0]
@ -51,8 +51,8 @@ func TestTeams_Send(t *testing.T) {
for _, fact := range mainSection.Facts { for _, fact := range mainSection.Facts {
switch fact.Name { switch fact.Name {
case configurationTypeFieldName: case phaseFieldName:
assert.Equal(t, fact.Value, event.ConfigurationType) assert.Equal(t, fact.Value, string(event.Phase))
case crNameFieldName: case crNameFieldName:
assert.Equal(t, fact.Value, event.Jenkins.Name) assert.Equal(t, fact.Value, event.Jenkins.Name)
case messageFieldName: case messageFieldName:

View File

@ -1,32 +1,53 @@
package notifications package notifications
import ( import (
"fmt"
"net/http" "net/http"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
"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"
) )
const ( const (
titleText = "Operator reconciled." infoTitleText = "Jenkins Operator reconciliation info"
warnTitleText = "Jenkins Operator reconciliation warning"
messageFieldName = "Message" messageFieldName = "Message"
loggingLevelFieldName = "Logging Level" loggingLevelFieldName = "Logging Level"
crNameFieldName = "CR Name" crNameFieldName = "CR Name"
configurationTypeFieldName = "Configuration Type" phaseFieldName = "Phase"
namespaceFieldName = "Namespace" namespaceFieldName = "Namespace"
footerContent = "Powered by Jenkins Operator" footerContent = "Powered by Jenkins Operator"
) )
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"
)
var ( var (
testConfigurationType = "test-configuration" testPhase = PhaseUser
testCrName = "test-cr" testCrName = "test-cr"
testNamespace = "default" testNamespace = "default"
testMessage = "test-message" testMessage = "test-message"
testMessageVerbose = "detail-test-message" testMessageVerbose = []string{"detail-test-message"}
testLoggingLevel = v1alpha2.NotificationLogLevelWarning testLoggingLevel = v1alpha2.NotificationLogLevelWarning
client = http.Client{} client = http.Client{}
) )
// Phase defines the type of configuration
type Phase string
// StatusColor is useful for better UX // StatusColor is useful for better UX
type StatusColor string type StatusColor string
@ -36,21 +57,21 @@ type LoggingLevel string
// Event contains event details which will be sent as a notification // Event contains event details which will be sent as a notification
type Event struct { type Event struct {
Jenkins v1alpha2.Jenkins Jenkins v1alpha2.Jenkins
ConfigurationType string Phase Phase
LogLevel v1alpha2.NotificationLogLevel LogLevel v1alpha2.NotificationLogLevel
Message string Message string
MessageVerbose string MessagesVerbose []string
} }
/*type service interface { type service interface {
Send(event Event, notificationConfig v1alpha2.Notification) error Send(event Event, notificationConfig v1alpha2.Notification) error
} }
// Listen listens for incoming events and send it as notifications // Listen listens for incoming events and send it as notifications
func Listen(events chan Event, k8sClient k8sclient.Client) { func Listen(events chan Event, k8sEvent event.Recorder, k8sClient k8sclient.Client) {
for event := range events { for evt := range events {
logger := log.Log.WithValues("cr", event.Jenkins.Name) logger := log.Log.WithValues("cr", evt.Jenkins.Name)
for _, notificationConfig := range event.Jenkins.Spec.Notifications { for _, notificationConfig := range evt.Jenkins.Spec.Notifications {
var err error var err error
var svc service var svc service
@ -61,23 +82,34 @@ func Listen(events chan Event, k8sClient k8sclient.Client) {
} else if notificationConfig.Mailgun != nil { } else if notificationConfig.Mailgun != nil {
svc = MailGun{k8sClient: k8sClient} svc = MailGun{k8sClient: k8sClient}
} else { } else {
logger.V(log.VWarn).Info(fmt.Sprintf("Unexpected notification `%+v`", notificationConfig)) logger.V(log.VWarn).Info(fmt.Sprintf("Unknown notification service `%+v`", notificationConfig))
continue continue
} }
go func(notificationConfig v1alpha2.Notification) { go func(notificationConfig v1alpha2.Notification) {
err = notify(svc, event, notificationConfig) err = notify(svc, evt, notificationConfig)
if err != nil { if err != nil {
if log.Debug { if log.Debug {
logger.Error(nil, fmt.Sprintf("%+v", errors.WithMessage(err, "failed to send notification"))) logger.Error(nil, fmt.Sprintf("%+v", errors.WithMessage(err, fmt.Sprintf("failed to send notification '%s'", notificationConfig.Name))))
} else { } else {
logger.Error(nil, fmt.Sprintf("%s", errors.WithMessage(err, "failed to send notification"))) logger.Error(nil, fmt.Sprintf("%s", errors.WithMessage(err, fmt.Sprintf("failed to send notification '%s'", notificationConfig.Name))))
} }
} }
}(notificationConfig) }(notificationConfig)
} }
k8sEvent.Emit(&evt.Jenkins, logLevelEventType(evt.LogLevel), "NotificationSent", evt.Message)
}
}
func logLevelEventType(level v1alpha2.NotificationLogLevel) event.Type {
switch level {
case v1alpha2.NotificationLogLevelWarning:
return event.TypeWarning
case v1alpha2.NotificationLogLevelInfo:
return event.TypeNormal
default:
return event.TypeNormal
} }
} }
@ -85,6 +117,15 @@ func notify(svc service, event Event, manifest v1alpha2.Notification) error {
if event.LogLevel == v1alpha2.NotificationLogLevelInfo && manifest.LoggingLevel == v1alpha2.NotificationLogLevelWarning { if event.LogLevel == v1alpha2.NotificationLogLevelInfo && manifest.LoggingLevel == v1alpha2.NotificationLogLevelWarning {
return nil return nil
} }
return svc.Send(event, manifest) return svc.Send(event, manifest)
}*/ }
func notificationTitle(event Event) string {
if event.LogLevel == v1alpha2.NotificationLogLevelInfo {
return infoTitleText
} else if event.LogLevel == v1alpha2.NotificationLogLevelWarning {
return warnTitleText
} else {
return ""
}
}

View File

@ -64,51 +64,61 @@ func (s Slack) Send(event Event, config v1alpha2.Notification) error {
return err return err
} }
slackMessage, err := json.Marshal(SlackMessage{ sm := &SlackMessage{
Attachments: []SlackAttachment{ Attachments: []SlackAttachment{
{ {
Fallback: "", Fallback: "",
Color: s.getStatusColor(event.LogLevel), Color: s.getStatusColor(event.LogLevel),
Text: titleText,
Fields: []SlackField{ Fields: []SlackField{
{ {
Title: messageFieldName, Title: "",
Value: event.Message, Value: event.Message,
Short: false, Short: false,
}, },
{
Title: crNameFieldName,
Value: event.Jenkins.Name,
Short: true,
},
{
Title: configurationTypeFieldName,
Value: event.ConfigurationType,
Short: true,
},
{
Title: loggingLevelFieldName,
Value: string(event.LogLevel),
Short: true,
},
{ {
Title: namespaceFieldName, Title: namespaceFieldName,
Value: event.Jenkins.Namespace, Value: event.Jenkins.Namespace,
Short: true, Short: true,
}, },
}, {
Footer: footerContent, Title: crNameFieldName,
Value: event.Jenkins.Name,
Short: true,
}, },
}, },
},
},
}
mainAttachment := &sm.Attachments[0]
mainAttachment.Title = notificationTitle(event)
if config.Verbose {
// TODO: or for title == message
message := event.Message
for _, msg := range event.MessagesVerbose {
message = message + "\n - " + msg
}
mainAttachment.Fields[0].Value = message
}
if event.Phase != PhaseUnknown {
mainAttachment.Fields = append(mainAttachment.Fields, SlackField{
Title: phaseFieldName,
Value: string(event.Phase),
Short: true,
}) })
}
slackMessage, err := json.Marshal(sm)
if err != nil {
return err
}
secretValue := string(secret.Data[selector.Key]) secretValue := string(secret.Data[selector.Key])
if secretValue == "" { if secretValue == "" {
return errors.Errorf("SecretValue %s is empty", selector.Name) return errors.Errorf("Slack WebHook URL is empty in secret '%s/%s[%s]", event.Jenkins.Namespace, selector.Name, selector.Key)
}
if err != nil {
return err
} }
request, err := http.NewRequest("POST", secretValue, bytes.NewBuffer(slackMessage)) request, err := http.NewRequest("POST", secretValue, bytes.NewBuffer(slackMessage))

View File

@ -27,9 +27,9 @@ func TestSlack_Send(t *testing.T) {
Namespace: testNamespace, Namespace: testNamespace,
}, },
}, },
ConfigurationType: testConfigurationType, Phase: testPhase,
Message: testMessage, Message: testMessage,
MessageVerbose: testMessageVerbose, MessagesVerbose: testMessageVerbose,
LogLevel: testLoggingLevel, LogLevel: testLoggingLevel,
} }
slack := Slack{k8sClient: fakeClient} slack := Slack{k8sClient: fakeClient}
@ -45,25 +45,25 @@ func TestSlack_Send(t *testing.T) {
mainAttachment := message.Attachments[0] mainAttachment := message.Attachments[0]
assert.Equal(t, mainAttachment.Text, titleText) assert.Equal(t, mainAttachment.Title, notificationTitle(event))
for _, field := range mainAttachment.Fields { for _, field := range mainAttachment.Fields {
switch field.Title { switch field.Title {
case configurationTypeFieldName: case phaseFieldName:
assert.Equal(t, field.Value, event.ConfigurationType) assert.Equal(t, field.Value, string(event.Phase))
case crNameFieldName: case crNameFieldName:
assert.Equal(t, field.Value, event.Jenkins.Name) assert.Equal(t, field.Value, event.Jenkins.Name)
case messageFieldName: case "":
assert.Equal(t, field.Value, event.Message) assert.Equal(t, field.Value, event.Message)
case loggingLevelFieldName: case loggingLevelFieldName:
assert.Equal(t, field.Value, string(event.LogLevel)) assert.Equal(t, field.Value, string(event.LogLevel))
case namespaceFieldName: case namespaceFieldName:
assert.Equal(t, field.Value, event.Jenkins.Namespace) assert.Equal(t, field.Value, event.Jenkins.Namespace)
default: default:
t.Fail() t.Errorf("Unexpected field %+v", field)
} }
} }
assert.Equal(t, mainAttachment.Footer, footerContent) assert.Equal(t, mainAttachment.Footer, "")
assert.Equal(t, mainAttachment.Color, slack.getStatusColor(event.LogLevel)) assert.Equal(t, mainAttachment.Color, slack.getStatusColor(event.LogLevel))
})) }))

View File

@ -1,14 +1,14 @@
package plugins package plugins
const ( const (
configurationAsCodePlugin = "configuration-as-code:1.29" configurationAsCodePlugin = "configuration-as-code:1.32"
configurationAsCodeSupportPlugin = "configuration-as-code-support:1.19" configurationAsCodeSupportPlugin = "configuration-as-code-support:1.19"
gitPlugin = "git:3.12.0" gitPlugin = "git:3.12.1"
jobDslPlugin = "job-dsl:1.76" jobDslPlugin = "job-dsl:1.76"
kubernetesCredentialsProviderPlugin = "kubernetes-credentials-provider:0.12.1" kubernetesCredentialsProviderPlugin = "kubernetes-credentials-provider:0.12.1"
kubernetesPlugin = "kubernetes:1.18.3" kubernetesPlugin = "kubernetes:1.19.3"
workflowAggregatorPlugin = "workflow-aggregator:2.6" workflowAggregatorPlugin = "workflow-aggregator:2.6"
workflowJobPlugin = "workflow-job:2.34" workflowJobPlugin = "workflow-job:2.35"
) )
// basePluginsList contains plugins to install by operator // basePluginsList contains plugins to install by operator

View File

@ -5,8 +5,6 @@ import (
"regexp" "regexp"
"strings" "strings"
"github.com/jenkinsci/kubernetes-operator/pkg/log"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -77,10 +75,10 @@ func Must(plugin *Plugin, err error) Plugin {
} }
// VerifyDependencies checks if all plugins have compatible versions // VerifyDependencies checks if all plugins have compatible versions
func VerifyDependencies(values ...map[Plugin][]Plugin) bool { func VerifyDependencies(values ...map[Plugin][]Plugin) []string {
var messages []string
// key - plugin name, value array of versions // key - plugin name, value array of versions
allPlugins := make(map[string][]Plugin) allPlugins := make(map[string][]Plugin)
valid := true
for _, value := range values { for _, value := range values {
for rootPlugin, plugins := range value { for rootPlugin, plugins := range value {
@ -105,18 +103,17 @@ func VerifyDependencies(values ...map[Plugin][]Plugin) bool {
for _, firstVersion := range versions { for _, firstVersion := range versions {
for _, secondVersion := range versions { for _, secondVersion := range versions {
if firstVersion.Version != secondVersion.Version { if firstVersion.Version != secondVersion.Version {
log.Log.V(log.VWarn).Info(fmt.Sprintf("Plugin '%s' requires version '%s' but plugin '%s' requires '%s' for plugin '%s'", messages = append(messages, fmt.Sprintf("Plugin '%s' requires version '%s' but plugin '%s' requires '%s' for plugin '%s'",
firstVersion.rootPluginNameAndVersion, firstVersion.rootPluginNameAndVersion,
firstVersion.Version, firstVersion.Version,
secondVersion.rootPluginNameAndVersion, secondVersion.rootPluginNameAndVersion,
secondVersion.Version, secondVersion.Version,
pluginName, pluginName,
)) ))
valid = false
} }
} }
} }
} }
return valid return messages
} }

View File

@ -18,7 +18,7 @@ func TestVerifyDependencies(t *testing.T) {
}, },
} }
got := VerifyDependencies(basePlugins) got := VerifyDependencies(basePlugins)
assert.Equal(t, true, got) assert.Nil(t, got)
}) })
t.Run("happy, two root plugins with one depended plugin with the same version", func(t *testing.T) { t.Run("happy, two root plugins with one depended plugin with the same version", func(t *testing.T) {
basePlugins := map[Plugin][]Plugin{ basePlugins := map[Plugin][]Plugin{
@ -30,7 +30,7 @@ func TestVerifyDependencies(t *testing.T) {
}, },
} }
got := VerifyDependencies(basePlugins) got := VerifyDependencies(basePlugins)
assert.Equal(t, true, got) assert.Nil(t, got)
}) })
t.Run("happy, two plugin names with names with underscores", func(t *testing.T) { t.Run("happy, two plugin names with names with underscores", func(t *testing.T) {
basePlugins := map[Plugin][]Plugin{ basePlugins := map[Plugin][]Plugin{
@ -42,7 +42,7 @@ func TestVerifyDependencies(t *testing.T) {
}, },
} }
got := VerifyDependencies(basePlugins) got := VerifyDependencies(basePlugins)
assert.Equal(t, true, got) assert.Nil(t, got)
}) })
t.Run("happy, two plugin names with uppercase names", func(t *testing.T) { t.Run("happy, two plugin names with uppercase names", func(t *testing.T) {
basePlugins := map[Plugin][]Plugin{ basePlugins := map[Plugin][]Plugin{
@ -54,7 +54,7 @@ func TestVerifyDependencies(t *testing.T) {
}, },
} }
got := VerifyDependencies(basePlugins) got := VerifyDependencies(basePlugins)
assert.Equal(t, true, got) assert.Nil(t, got)
}) })
t.Run("fail, two root plugins have different versions", func(t *testing.T) { t.Run("fail, two root plugins have different versions", func(t *testing.T) {
basePlugins := map[Plugin][]Plugin{ basePlugins := map[Plugin][]Plugin{
@ -66,7 +66,8 @@ func TestVerifyDependencies(t *testing.T) {
}, },
} }
got := VerifyDependencies(basePlugins) got := VerifyDependencies(basePlugins)
assert.Equal(t, false, got) 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 sperate plugins lists", func(t *testing.T) { t.Run("happy, no version collision with two sperate plugins lists", func(t *testing.T) {
basePlugins := map[Plugin][]Plugin{ basePlugins := map[Plugin][]Plugin{
@ -80,7 +81,7 @@ func TestVerifyDependencies(t *testing.T) {
}, },
} }
got := VerifyDependencies(basePlugins, extraPlugins) got := VerifyDependencies(basePlugins, extraPlugins)
assert.Equal(t, true, got) assert.Nil(t, got)
}) })
t.Run("fail, dependent plugins have different versions", func(t *testing.T) { t.Run("fail, dependent plugins have different versions", func(t *testing.T) {
basePlugins := map[Plugin][]Plugin{ basePlugins := map[Plugin][]Plugin{
@ -92,7 +93,10 @@ func TestVerifyDependencies(t *testing.T) {
}, },
} }
got := VerifyDependencies(basePlugins) got := VerifyDependencies(basePlugins)
assert.Equal(t, false, got) 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) { t.Run("fail, root and dependent plugins have different versions", func(t *testing.T) {
basePlugins := map[Plugin][]Plugin{ basePlugins := map[Plugin][]Plugin{
@ -106,7 +110,10 @@ func TestVerifyDependencies(t *testing.T) {
}, },
} }
got := VerifyDependencies(basePlugins, extraPlugins) got := VerifyDependencies(basePlugins, extraPlugins)
assert.Equal(t, false, got) 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) { t.Run("happy with dash in version", func(t *testing.T) {
basePlugins := map[Plugin][]Plugin{ basePlugins := map[Plugin][]Plugin{

View File

@ -17,7 +17,7 @@ apiVersion: v1
kind: PersistentVolumeClaim kind: PersistentVolumeClaim
metadata: metadata:
name: <pvc_name> name: <pvc_name>
namespace: <namesapce> namespace: <namespace>
spec: spec:
accessModes: accessModes:
- ReadWriteOnce - ReadWriteOnce
@ -28,7 +28,7 @@ spec:
Run command: Run command:
```bash ```bash
$ kubectl -n <namesapce> create -f pvc.yaml $ kubectl -n <namespace> create -f pvc.yaml
``` ```
#### Configure Jenkins CR #### Configure Jenkins CR

View File

@ -19,7 +19,7 @@ apiVersion: v1
kind: PersistentVolumeClaim kind: PersistentVolumeClaim
metadata: metadata:
name: <pvc_name> name: <pvc_name>
namespace: <namesapce> namespace: <namespace>
spec: spec:
accessModes: accessModes:
- ReadWriteOnce - ReadWriteOnce
@ -30,7 +30,7 @@ spec:
Run command: Run command:
```bash ```bash
$ kubectl -n <namesapce> create -f pvc.yaml $ kubectl -n <namespace> create -f pvc.yaml
``` ```
#### Configure Jenkins CR #### Configure Jenkins CR