Merge branch 'master' into fix-plugin-version
This commit is contained in:
		
						commit
						26cf1242ea
					
				|  | @ -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 | ||||||
|  |  | ||||||
							
								
								
									
										6
									
								
								Makefile
								
								
								
								
							
							
						
						
									
										6
									
								
								Makefile
								
								
								
								
							|  | @ -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) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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
								
								
								
								
							
							
						
						
									
										8
									
								
								go.mod
								
								
								
								
							|  | @ -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
								
								
								
								
							
							
						
						
									
										7
									
								
								go.sum
								
								
								
								
							|  | @ -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= | ||||||
|  |  | ||||||
|  | @ -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 :
 | ||||||
|  |  | ||||||
|  | @ -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) | ||||||
|  |  | ||||||
|  | @ -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" | ||||||
|  |  | ||||||
|  | @ -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
 | ||||||
|  |  | ||||||
|  | @ -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 { | ||||||
|  |  | ||||||
|  | @ -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) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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"}) | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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 { | ||||||
|  |  | ||||||
|  | @ -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"}) | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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) | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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 { | ||||||
|  |  | ||||||
|  | @ -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() | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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: | ||||||
|  |  | ||||||
|  | @ -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 "" | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -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)) | ||||||
|  |  | ||||||
|  | @ -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)) | ||||||
| 	})) | 	})) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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
 | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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{ | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue