commit
5e17461728
|
|
@ -0,0 +1,52 @@
|
|||
FROM docker:18.09
|
||||
|
||||
ARG GO_VERSION
|
||||
ARG OPERATOR_SDK_VERSION
|
||||
ARG MINIKUBE_VERSION
|
||||
|
||||
ARG GOPATH="/go"
|
||||
|
||||
RUN mkdir -p /go
|
||||
|
||||
# Stage 1 - Install dependencies
|
||||
RUN apk update && \
|
||||
apk add --no-cache \
|
||||
curl \
|
||||
python \
|
||||
py-crcmod \
|
||||
bash \
|
||||
libc6-compat \
|
||||
openssh-client \
|
||||
git \
|
||||
make \
|
||||
gcc \
|
||||
libc-dev \
|
||||
git
|
||||
|
||||
RUN curl -O https://storage.googleapis.com/golang/go$GO_VERSION.linux-amd64.tar.gz && tar -xvf go$GO_VERSION.linux-amd64.tar.gz
|
||||
|
||||
# Stage 2 - Install operator-sdk
|
||||
RUN echo $GOPATH/bin/operator-sdk
|
||||
RUN curl -L https://github.com/operator-framework/operator-sdk/releases/download/v$OPERATOR_SDK_VERSION/operator-sdk-v$OPERATOR_SDK_VERSION-x86_64-linux-gnu -o $GOPATH/bin/operator-sdk \
|
||||
&& chmod +x $GOPATH/bin/operator-sdk
|
||||
|
||||
RUN curl -Lo minikube https://storage.googleapis.com/minikube/releases/v$MINIKUBE_VERSION/minikube-linux-amd64 \
|
||||
&& chmod +x minikube \
|
||||
&& cp minikube /usr/local/bin/ \
|
||||
&& rm minikube
|
||||
|
||||
RUN curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl \
|
||||
&& chmod +x ./kubectl \
|
||||
&& mv ./kubectl /usr/local/bin/kubectl
|
||||
|
||||
RUN export GO111MODULE=auto
|
||||
|
||||
RUN mkdir -p $GOPATH/src/github.com/jenkinsci/kubernetes-operator
|
||||
WORKDIR $GOPATH/src/github.com/jenkinsci/kubernetes-operator
|
||||
|
||||
RUN mkdir -p /home/builder
|
||||
|
||||
ENV DOCKER_TLS_VERIFY 1
|
||||
ENV DOCKER_CERT_PATH /minikube/certs
|
||||
|
||||
ENTRYPOINT ["./entrypoint.sh"]
|
||||
64
Makefile
64
Makefile
|
|
@ -2,6 +2,19 @@
|
|||
SHELL := /bin/sh
|
||||
PATH := $(GOPATH)/bin:$(PATH)
|
||||
|
||||
OSFLAG :=
|
||||
ifeq ($(OS),Windows_NT)
|
||||
OSFLAG = WIN32
|
||||
else
|
||||
UNAME_S := $(shell uname -s)
|
||||
ifeq ($(UNAME_S),Linux)
|
||||
OSFLAG = LINUX
|
||||
endif
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
OSFLAG = OSX
|
||||
endif
|
||||
endif
|
||||
|
||||
# Import config
|
||||
# You can change the default config with `make config="config_special.env" build`
|
||||
config ?= config.env
|
||||
|
|
@ -149,7 +162,7 @@ prepare-all-in-one-deploy-file: ## Prepares all in one deploy file
|
|||
|
||||
.PHONY: e2e
|
||||
CURRENT_DIRECTORY := $(shell pwd)
|
||||
e2e: build docker-build ## Runs e2e tests, you can use EXTRA_ARGS
|
||||
e2e: docker-build ## Runs e2e tests, you can use EXTRA_ARGS
|
||||
@echo "+ $@"
|
||||
@echo "Docker image: $(DOCKER_REGISTRY):$(GITCOMMIT)"
|
||||
kubectl config use-context $(KUBECTL_CONTEXT)
|
||||
|
|
@ -157,10 +170,20 @@ e2e: build docker-build ## Runs e2e tests, you can use EXTRA_ARGS
|
|||
cat deploy/role.yaml >> deploy/namespace-init.yaml
|
||||
cat deploy/role_binding.yaml >> deploy/namespace-init.yaml
|
||||
cat deploy/operator.yaml >> deploy/namespace-init.yaml
|
||||
ifeq ($(OSFLAG), LINUX)
|
||||
sed -i 's|\(image:\).*|\1 $(DOCKER_REGISTRY):$(GITCOMMIT)|g' deploy/namespace-init.yaml
|
||||
ifeq ($(KUBECTL_CONTEXT),minikube)
|
||||
sed -i 's|\(imagePullPolicy\): IfNotPresent|\1: Never|g' deploy/namespace-init.yaml
|
||||
sed -i 's|\(args:\).*|\1\ ["--minikube"\]|' deploy/namespace-init.yaml
|
||||
endif
|
||||
endif
|
||||
|
||||
ifeq ($(OSFLAG), OSX)
|
||||
sed -i '' 's|\(image:\).*|\1 $(DOCKER_REGISTRY):$(GITCOMMIT)|g' deploy/namespace-init.yaml
|
||||
ifeq ($(KUBECTL_CONTEXT),minikube)
|
||||
sed -i '' 's|\(imagePullPolicy\): IfNotPresent|\1: Never|g' deploy/namespace-init.yaml
|
||||
sed -i '' 's|\(args:\).*|\1\ ["--minikube"\]|' deploy/namespace-init.yaml
|
||||
endif
|
||||
endif
|
||||
|
||||
@RUNNING_TESTS=1 go test -parallel=1 "./test/e2e/" -tags "$(BUILDTAGS) cgo" -v -timeout 30m -run "$(E2E_TEST_SELECTOR)" \
|
||||
|
|
@ -173,13 +196,14 @@ vet: ## Verifies `go vet` passes
|
|||
|
||||
.PHONY: staticcheck
|
||||
HAS_STATICCHECK := $(shell which staticcheck)
|
||||
PLATFORM = $(shell echo $(UNAME_S) | tr A-Z a-z)
|
||||
staticcheck: ## Verifies `staticcheck` passes
|
||||
@echo "+ $@"
|
||||
ifndef HAS_STATICCHECK
|
||||
wget https://github.com/dominikh/go-tools/releases/download/2019.1.1/staticcheck_linux_amd64
|
||||
chmod +x staticcheck_linux_amd64
|
||||
mkdir -p $(HOME)/bin
|
||||
mv staticcheck_linux_amd64 $(HOME)/bin/staticcheck
|
||||
wget https://github.com/dominikh/go-tools/releases/download/2019.1.1/staticcheck_$(PLATFORM)_amd64
|
||||
chmod +x staticcheck_$(PLATFORM)_amd64
|
||||
mkdir -p $(GOPATH)/bin
|
||||
mv staticcheck_$(PLATFORM)_amd64 $(GOPATH)/bin/staticcheck
|
||||
endif
|
||||
@staticcheck $(PACKAGES)
|
||||
|
||||
|
|
@ -208,8 +232,8 @@ run: export WATCH_NAMESPACE = $(NAMESPACE)
|
|||
run: export OPERATOR_NAME = $(NAME)
|
||||
run: build ## Run the executable, you can use EXTRA_ARGS
|
||||
@echo "+ $@"
|
||||
kubectl apply -f deploy/crds/jenkins_$(API_VERSION)_jenkins_crd.yaml
|
||||
kubectl config use-context $(KUBECTL_CONTEXT)
|
||||
kubectl apply -f deploy/crds/jenkins_$(API_VERSION)_jenkins_crd.yaml
|
||||
@echo "Watching '$(WATCH_NAMESPACE)' namespace"
|
||||
build/_output/bin/jenkins-operator --local $(EXTRA_ARGS)
|
||||
|
||||
|
|
@ -267,7 +291,7 @@ docker-login: ## Log in into the Docker repository
|
|||
@echo "+ $@"
|
||||
|
||||
.PHONY: docker-build
|
||||
docker-build: check-env build ## Build the container
|
||||
docker-build: check-env ## Build the container
|
||||
@echo "+ $@"
|
||||
docker build . -t $(DOCKER_REGISTRY):$(GITCOMMIT) --file build/Dockerfile
|
||||
|
||||
|
|
@ -316,7 +340,7 @@ docker-run: ## Run the container in docker, you can use EXTRA_ARGS
|
|||
.PHONY: minikube-run
|
||||
minikube-run: export WATCH_NAMESPACE = $(NAMESPACE)
|
||||
minikube-run: export OPERATOR_NAME = $(NAME)
|
||||
minikube-run: minikube-start build ## Run the operator locally and use minikube as Kubernetes cluster, you can use EXTRA_ARGS
|
||||
minikube-run: minikube-start ## Run the operator locally and use minikube as Kubernetes cluster, you can use EXTRA_ARGS
|
||||
@echo "+ $@"
|
||||
kubectl config use-context minikube
|
||||
kubectl apply -f deploy/crds/jenkins_$(API_VERSION)_jenkins_crd.yaml
|
||||
|
|
@ -390,3 +414,27 @@ endif
|
|||
@echo "Dependencies:"
|
||||
go mod vendor -v
|
||||
@echo
|
||||
|
||||
.PHONY: image
|
||||
image: ## Create the docker image from the Dockerfile. This image is used to build linux binary regardless of the system on the host
|
||||
@echo "+ $@"
|
||||
docker build --rm --force-rm --no-cache \
|
||||
--build-arg GO_VERSION=$(GO_VERSION) \
|
||||
--build-arg MINIKUBE_VERSION=$(MINIKUBE_VERSION) \
|
||||
--build-arg OPERATOR_SDK_VERSION=$(OPERATOR_SDK_VERSION) \
|
||||
-t jenkins-operator/runner .
|
||||
|
||||
.PHONY: indocker
|
||||
PWD := $(shell pwd)
|
||||
DOCKER_HOST_IP := $(shell minikube docker-env | grep DOCKER_HOST | cut -d '"' -f 2)
|
||||
MINIKUBE_IP := $(shell minikube ip)
|
||||
indocker: minikube-start image ## Run make in a docker container
|
||||
@echo "+ $@"
|
||||
docker run --rm -it $(DOCKER_FLAGS) \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--mount type=bind,source=$(PWD),target=/go/src/github.com/jenkinsci/kubernetes-operator \
|
||||
--mount type=bind,source=$(HOME)/.minikube,target=/minikube \
|
||||
--mount type=bind,source=$(HOME)/.kube,target=/home/builder/.kube \
|
||||
-e DOCKER_HOST=$(DOCKER_HOST_IP) \
|
||||
-e MINIKUBE_IP=$(MINIKUBE_IP) \
|
||||
jenkins-operator/runner
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
# Setup variables for the Makefile
|
||||
NAME=kubernetes-operator
|
||||
OPERATOR_SDK_VERSION=0.8.1
|
||||
GO_VERSION=1.12.6
|
||||
PKG=github.com/jenkinsci/kubernetes-operator
|
||||
DOCKER_ORGANIZATION=virtuslab
|
||||
DOCKER_REGISTRY=jenkins-operator
|
||||
|
|
@ -7,6 +9,7 @@ NAMESPACE=default
|
|||
API_VERSION=v1alpha2
|
||||
MINIKUBE_KUBERNETES_VERSION=v1.12.9
|
||||
MINIKUBE_DRIVER=virtualbox
|
||||
MINIKUBE_VERSION=1.2.0
|
||||
KUBECTL_CONTEXT=minikube
|
||||
ALL_IN_ONE_DEPLOY_FILE_PREFIX=all-in-one
|
||||
GEN_CRD_API=gen-crd-api-reference-docs
|
||||
|
|
@ -32,3 +32,4 @@ spec:
|
|||
fieldPath: metadata.name
|
||||
- name: OPERATOR_NAME
|
||||
value: "jenkins-operator"
|
||||
|
||||
|
|
|
|||
|
|
@ -5,12 +5,13 @@ This document describes a getting started guide for **jenkins-operator** and an
|
|||
1. [First Steps](#first-steps)
|
||||
2. [Deploy Jenkins](#deploy-jenkins)
|
||||
3. [Configure Seed Jobs and Pipelines](#configure-seed-jobs-and-pipelines)
|
||||
4. [Install Plugins](#install-plugins)
|
||||
5. [Configure Backup & Restore](#configure-backup-and-restore)
|
||||
6. [AKS](#aks)
|
||||
7. [Jenkins login credentials](#jenkins-login-credentials)
|
||||
8. [Override default Jenkins container command](#override-default-Jenkins-container-command)
|
||||
9. [Debugging](#debugging)
|
||||
4. [Pulling Docker images from private repositories](#pulling-docker-images-from-private-repositories)
|
||||
5. [Install Plugins](#install-plugins)
|
||||
6. [Configure Backup & Restore](#configure-backup-and-restore)
|
||||
7. [AKS](#aks)
|
||||
8. [Jenkins login credentials](#jenkins-login-credentials)
|
||||
9. [Override default Jenkins container command](#override-default-Jenkins-container-command)
|
||||
10. [Debugging](#debugging)
|
||||
|
||||
## First Steps
|
||||
|
||||
|
|
@ -293,35 +294,109 @@ data:
|
|||
password: password_or_token
|
||||
```
|
||||
|
||||
## Pulling Docker images from private repositories
|
||||
To pull Docker Image from private repository you can use `imagePullSecrets`.
|
||||
|
||||
Please follow the instructions on [creating a secret with a docker config](https://kubernetes.io/docs/concepts/containers/images/?origin_team=T42NTAGHM#creating-a-secret-with-a-docker-config).
|
||||
|
||||
### Docker Hub Configuration
|
||||
To use Docker Hub additional steps are required.
|
||||
|
||||
Edit the previously created secret:
|
||||
```bash
|
||||
kubectl -n <namespace> edit secret <name>
|
||||
```
|
||||
|
||||
The `.dockerconfigjson` key's value needs to be replaced with a modified version.
|
||||
|
||||
After modifications it needs to be encoded as Base64 value before setting the `.dockerconfigjson` key:q.
|
||||
|
||||
Example config file to modify and use:
|
||||
```
|
||||
{
|
||||
"auths":{
|
||||
"https://index.docker.io/v1/":{
|
||||
"username":"user",
|
||||
"password":"password",
|
||||
"email":"yourdockeremail@gmail.com",
|
||||
"auth":"base64 of string user:password"
|
||||
},
|
||||
"auth.docker.io":{
|
||||
"username":"user",
|
||||
"password":"password",
|
||||
"email":"yourdockeremail@gmail.com",
|
||||
"auth":"base64 of string user:password"
|
||||
},
|
||||
"registry.docker.io":{
|
||||
"username":"user",
|
||||
"password":"password",
|
||||
"email":"yourdockeremail@gmail.com",
|
||||
"auth":"base64 of string user:password"
|
||||
},
|
||||
"docker.io":{
|
||||
"username":"user",
|
||||
"password":"password",
|
||||
"email":"yourdockeremail@gmail.com",
|
||||
"auth":"base64 of string user:password"
|
||||
},
|
||||
"https://registry-1.docker.io/v2/": {
|
||||
"username":"user",
|
||||
"password":"password",
|
||||
"email":"yourdockeremail@gmail.com",
|
||||
"auth":"base64 of string user:password"
|
||||
},
|
||||
"registry-1.docker.io/v2/": {
|
||||
"username":"user",
|
||||
"password":"password",
|
||||
"email":"yourdockeremail@gmail.com",
|
||||
"auth":"base64 of string user:password"
|
||||
},
|
||||
"registry-1.docker.io": {
|
||||
"username":"user",
|
||||
"password":"password",
|
||||
"email":"yourdockeremail@gmail.com",
|
||||
"auth":"base64 of string user:password"
|
||||
},
|
||||
"https://registry-1.docker.io": {
|
||||
"username":"user",
|
||||
"password":"password",
|
||||
"email":"yourdockeremail@gmail.com",
|
||||
"auth":"base64 of string user:password"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Jenkins Customisation
|
||||
|
||||
Jenkins can be customized using groovy scripts or configuration as code plugin. All custom configuration is stored in
|
||||
the **jenkins-operator-user-configuration-<cr_name>** ConfigMap which is automatically created by **jenkins-operator**.
|
||||
Jenkins can be customized using groovy scripts or [configuration as code plugin](https://github.com/jenkinsci/configuration-as-code-plugin).
|
||||
By using [ConfigMap](https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/) you can create own **Jenkins** customized configuration.
|
||||
Then you must reference the *ConfigMap* in **Jenkins** pod customization file in `spec.groovyScripts` or `spec.configurationAsCode`
|
||||
|
||||
**jenkins-operator** creates **jenkins-operator-user-configuration-<cr_name>** secret where user can store sensitive
|
||||
information used for custom configuration. If you have entry in secret named `PASSWORD` then you can use it in
|
||||
Configuration as Plugin as `adminAddress: "${PASSWORD}"`.
|
||||
For example create *ConfigMap* with name `jenkins-operator-user-configuration`. Then, modify the **Jenkins** manifest to look like this:
|
||||
|
||||
```
|
||||
kubectl get secret jenkins-operator-user-configuration-<cr_name> -o yaml
|
||||
|
||||
kind: Secret
|
||||
apiVersion: v1
|
||||
type: Opaque
|
||||
```yaml
|
||||
apiVersion: jenkins.io/v1alpha2
|
||||
kind: Jenkins
|
||||
metadata:
|
||||
name: jenkins-operator-user-configuration-<cr_name>
|
||||
namespace: default
|
||||
data:
|
||||
SECRET_JENKINS_ADMIN_ADDRESS: YXNkZgo=
|
||||
|
||||
name: example
|
||||
spec:
|
||||
configurationAsCode:
|
||||
configurations:
|
||||
- name: jenkins-operator-user-configuration
|
||||
groovyScripts:
|
||||
configurations:
|
||||
- name: jenkins-operator-user-configuration
|
||||
```
|
||||
|
||||
```
|
||||
kubectl get configmap jenkins-operator-user-configuration-<cr_name> -o yaml
|
||||
|
||||
Here is example of `jenkins-operator-user-configuration`:
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: jenkins-operator-user-configuration
|
||||
data:
|
||||
1-configure-theme.groovy: |2
|
||||
1-configure-theme.groovy: |
|
||||
import jenkins.*
|
||||
import jenkins.model.*
|
||||
import hudson.*
|
||||
|
|
@ -341,19 +416,88 @@ data:
|
|||
decorator.save();
|
||||
|
||||
jenkins.save()
|
||||
1-system-message.yaml: |2
|
||||
1-system-message.yaml: |
|
||||
jenkins:
|
||||
systemMessage: "Configuration as Code integration works!!!"
|
||||
adminAddress: "${SECRET_JENKINS_ADMIN_ADDRESS}"
|
||||
```
|
||||
|
||||
* *.groovy is Groovy script configuration
|
||||
* *.yaml is configuration as code
|
||||
|
||||
If you want to correct your configuration you can edit it while **jenkins-operator** is running.
|
||||
Jenkins will reconcile and apply new configuration.
|
||||
|
||||
### Using secrets inside Groovy script
|
||||
|
||||
If you configured `spec.groovyScripts.secret.name`, then this secret is available to use inside map Groovy scripts.
|
||||
The secrets are loaded to `secrets` map.
|
||||
|
||||
Create a [secret](https://kubernetes.io/docs/concepts/configuration/secret/) with for eg. `jenkins-conf-secrets` name.
|
||||
|
||||
```yaml
|
||||
kind: Secret
|
||||
apiVersion: v1
|
||||
type: Opaque
|
||||
metadata:
|
||||
name: jenkins-conf-secrets
|
||||
namespace: default
|
||||
data:
|
||||
SYSTEM_MESSAGE: SGVsbG8gd29ybGQ=
|
||||
```
|
||||
|
||||
Then modify the **Jenkins** pod manifest by changing `spec.groovyScripts.secret.name` to `jenkins-conf-secrets`.
|
||||
|
||||
```yaml
|
||||
apiVersion: jenkins.io/v1alpha2
|
||||
kind: Jenkins
|
||||
metadata:
|
||||
name: example
|
||||
spec:
|
||||
configurationAsCode:
|
||||
configurations:
|
||||
- name: jenkins-operator-user-configuration
|
||||
secret:
|
||||
name: jenkins-conf-secrets
|
||||
groovyScripts:
|
||||
configurations:
|
||||
- name: jenkins-operator-user-configuration
|
||||
secret:
|
||||
name: jenkins-conf-secrets
|
||||
```
|
||||
|
||||
Now you can test that the secret is mounted by applying this ConfigMap for Groovy script:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: jenkins-operator-user-configuration-<cr_name>
|
||||
namespace: default
|
||||
```
|
||||
name: jenkins-operator-user-configuration
|
||||
data:
|
||||
1-system-message.groovy: |
|
||||
import jenkins.*
|
||||
import jenkins.model.*
|
||||
import hudson.*
|
||||
import hudson.model.*
|
||||
Jenkins jenkins = Jenkins.getInstance()
|
||||
|
||||
jenkins.setSystemMessage(secrets["SYSTEM_MESSAGE"])
|
||||
jenkins.save()
|
||||
```
|
||||
|
||||
When **jenkins-operator-user-configuration-<cr_name>** ConfigMap is updated Jenkins automatically
|
||||
runs the **jenkins-operator-user-configuration** Jenkins Job which executes all scripts then
|
||||
runs the **jenkins-operator-user-configuration-casc** Jenkins Job which applies Configuration as Code configuration.
|
||||
Or by applying configuration as code:
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: jenkins-operator-user-configuration
|
||||
data:
|
||||
1-system-message.yaml: |
|
||||
jenkins:
|
||||
systemMessage: ${SYSTEM_MESSAGE}
|
||||
```
|
||||
|
||||
|
||||
After this, you should see the `Hello world` system message at **Jenkins** homepage.
|
||||
|
||||
## Install Plugins
|
||||
|
||||
|
|
|
|||
|
|
@ -308,7 +308,7 @@ To use default CRD file:
|
|||
kubectl -n <namespace> apply -f https://github.com/jenkinsci/kubernetes-operator/blob/master/deploy/crds/jenkins_v1alpha2_jenkins_crd.yaml
|
||||
```
|
||||
|
||||
## Update RBAC to new verison
|
||||
## Update RBAC to new version
|
||||
|
||||
New operator version requires updated RBAC permissions:
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,58 @@ Kubernetes API permissions are limited by the following roles:
|
|||
- [jenkins-operator role](../deploy/role.yaml)
|
||||
- [Jenkins Master role](../pkg/controller/jenkins/configuration/base/resources/rbac.go)
|
||||
|
||||
Since **jenkins-operator** must be able to grant permission for its' deployed Jenkins masters to spawn pods (the `Jenkins Master role` above), the operator itself requires permission to create RBAC resources (the `jenkins-operator role` above). Deployed this way, any subject which may create a Pod (including a Jenkins job) may assume the `jenkins-operator` role by using its' ServiceAccount, create RBAC rules, and thus escape its granted permissions. Any namespace to which the `jenkins-operator` is deployed must be considered to implicitly grant all possible permissions to any subject which can create a Pod in that namespace.
|
||||
Since **jenkins-operator** must be able to grant permission for its' deployed Jenkins masters to spawn pods (the `Jenkins Master role` above),
|
||||
the operator itself requires permission to create RBAC resources (the `jenkins-operator role` above).
|
||||
Deployed this way, any subject which may create a Pod (including a Jenkins job) may
|
||||
assume the `jenkins-operator` role by using its' ServiceAccount, create RBAC rules, and thus escape its granted permissions.
|
||||
Any namespace to which the `jenkins-operator` is deployed must be considered to implicitly grant all
|
||||
possible permissions to any subject which can create a Pod in that namespace.
|
||||
|
||||
To mitigate this issue **jenkins-operator** should be deployed in one namespace and the Jenkins CR should be created in separate namespace.
|
||||
To achieve it change watch namespace in https://github.com/jenkinsci/kubernetes-operator/blob/master/deploy/operator.yaml#L25
|
||||
|
||||
## Setup Jenkins Operator and Jenkins in separated namespaces
|
||||
|
||||
You need to create two namespaces, for example we'll call them **jenkins** for Jenkins and **jenkins-operator** for Jenkins Operator.
|
||||
```bash
|
||||
$ kubectl create ns jenkins-operator
|
||||
$ kubectl create ns jenkins
|
||||
```
|
||||
|
||||
Next, apply the RBAC manifests for **jenkins-operator** namespace
|
||||
```bash
|
||||
$ kubectl -n jenkins-operator apply -f deploy/service_account.yaml
|
||||
$ kubectl -n jenkins-operator apply -f deploy/role_binding.yaml
|
||||
```
|
||||
|
||||
Create file role_binding_jenkins.yaml in `deploy` folder:
|
||||
```yaml
|
||||
kind: RoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: jenkins-operator
|
||||
namespace: jenkins
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: jenkins-operator
|
||||
namespace: jenkins-operator
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: jenkins-operator
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
```
|
||||
|
||||
Then, apply RBAC rules for **jenkins** namespace
|
||||
```bash
|
||||
$ kubectl -n jenkins apply -f deploy/role.yaml
|
||||
$ kubectl -n jenkins apply -f role_binding_jenkins.yaml
|
||||
```
|
||||
|
||||
Finally, you must create operator pod by:
|
||||
```bash
|
||||
$ kubectl -n jenkins -n jenkins-operator apply -f deploy/operator.yaml
|
||||
```
|
||||
|
||||
|
||||
## Report a Security Vulnerability
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
#!/bin/bash
|
||||
|
||||
export GOPATH=/go
|
||||
export PATH=$PATH:/usr/local/go/bin:$GOPATH/bin
|
||||
export GO111MODULE=on
|
||||
|
||||
kubectl config set-cluster minikube --server=https://$MINIKUBE_IP:8443 \
|
||||
--certificate-authority=/minikube/ca.crt && \
|
||||
kubectl config set-credentials minikube --certificate-authority=/root/.minikube/ca.crt \
|
||||
--client-key=/minikube/client.key \
|
||||
--client-certificate=/minikube/client.crt && \
|
||||
kubectl config set-context minikube --cluster=minikube --user=minikube && \
|
||||
kubectl config use-context minikube
|
||||
|
||||
make go-dependencies
|
||||
ln -s $GOPATH/src/github.com/jenkinsci/kubernetes-operator/vendor/k8s.io $GOPATH/src/k8s.i
|
||||
ln -s $GOPATH/src/github.com/jenkinsci/kubernetes-operator/vendor/sigs.k8s.io $GOPATH/src/sigs.k8s.io
|
||||
|
||||
bash
|
||||
11
go.mod
11
go.mod
|
|
@ -9,6 +9,8 @@ require (
|
|||
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
|
||||
github.com/docker/distribution v2.7.1+incompatible
|
||||
github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c // indirect
|
||||
github.com/elazarl/goproxy v0.0.0-20190711103511-473e67f1d7d2 // indirect
|
||||
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 // indirect
|
||||
github.com/emicklei/go-restful v2.8.1+incompatible // indirect
|
||||
github.com/go-logr/logr v0.1.0
|
||||
github.com/go-logr/zapr v0.1.0
|
||||
|
|
@ -22,19 +24,26 @@ require (
|
|||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.8.5 // indirect
|
||||
github.com/imdario/mergo v0.3.6 // indirect
|
||||
github.com/mailgun/mailgun-go/v3 v3.6.0
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
|
||||
github.com/operator-framework/operator-sdk v0.8.2-0.20190522220659-031d71ef8154
|
||||
github.com/pborman/uuid v0.0.0-20180906182336-adf5a7427709 // indirect
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/sergi/go-diff v1.0.0 // indirect
|
||||
github.com/spf13/pflag v1.0.3
|
||||
github.com/stretchr/testify v1.3.0
|
||||
go.opencensus.io v0.19.2 // indirect
|
||||
go.uber.org/atomic v1.3.2 // indirect
|
||||
go.uber.org/multierr v1.1.0 // indirect
|
||||
go.uber.org/zap v1.9.1
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 // indirect
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7 // indirect
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb // indirect
|
||||
golang.org/x/text v0.3.2 // indirect
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 // indirect
|
||||
golang.org/x/tools v0.0.0-20190708203411-c8855242db9c // indirect
|
||||
k8s.io/api v0.0.0-20190222213804-5cb15d344471
|
||||
k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628
|
||||
k8s.io/client-go v2.0.0-alpha.0.0.20181126152608-d082d5923d3c+incompatible
|
||||
|
|
@ -62,3 +71,5 @@ replace (
|
|||
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
|
||||
)
|
||||
|
||||
replace github.com/golang/lint v0.0.0-20190409202823-959b441ac422 => golang.org/x/lint v0.0.0-20190409202823-959b441ac422
|
||||
|
|
|
|||
45
go.sum
45
go.sum
|
|
@ -40,13 +40,26 @@ github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c/go.mod h1:Qh8CwZ
|
|||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/elazarl/goproxy v0.0.0-20190711103511-473e67f1d7d2 h1:aZtFdDNWY/yH86JPR2WX/PN63635VsE/f/nXNPAbYxY=
|
||||
github.com/elazarl/goproxy v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
||||
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM=
|
||||
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
|
||||
github.com/emicklei/go-restful v2.8.1+incompatible h1:AyDqLHbJ1quqbWr/OWDw+PlIP8ZFoTmYrGYaxzrLbNg=
|
||||
github.com/emicklei/go-restful v2.8.1+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
||||
github.com/evanphx/json-patch v4.0.0+incompatible h1:xregGRMLBeuRcwiOTHRCsPPuzCQlqhxUPbqdw+zNkLc=
|
||||
github.com/evanphx/json-patch v4.0.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ=
|
||||
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64=
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A=
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
|
||||
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y=
|
||||
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-chi/chi v4.0.0+incompatible h1:SiLLEDyAkqNnw+T/uDTf3aFB9T4FTrwMpuYrgaRcnW4=
|
||||
github.com/go-chi/chi v4.0.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logr/logr v0.1.0 h1:M1Tv3VzNlEHg6uyACnRdtrploV2P7wZqH8BoQMtz0cg=
|
||||
|
|
@ -83,6 +96,7 @@ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
|
|||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck=
|
||||
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
|
||||
|
|
@ -101,6 +115,7 @@ github.com/grpc-ecosystem/grpc-gateway v1.8.5 h1:2+KSC78XiO6Qy0hIjfc1OD9H+hsaJdJ
|
|||
github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
|
||||
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
|
|
@ -116,10 +131,14 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
|
|||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/mailgun/mailgun-go/v3 v3.6.0 h1:oQWhyDTFjSiuO6vx1PRlfLZ7Fu+oK0Axn0UTREh3k/g=
|
||||
github.com/mailgun/mailgun-go/v3 v3.6.0/go.mod h1:E81I5Agcfi/u1szdehi6p6ttdRX/UD3Rq2SrUzwyFIU=
|
||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic=
|
||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/markbates/inflect v1.0.4 h1:5fh1gzTFhfae06u3hzHYO9xe3l3v3nW5Pwt3naLTP5g=
|
||||
|
|
@ -134,7 +153,9 @@ github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9
|
|||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||
|
|
@ -165,9 +186,12 @@ github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1 h1:/K3IL0Z1quvmJ
|
|||
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc=
|
||||
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.2.2 h1:J7U/N7eRtzjhs26d6GqMh2HBuXP8/Z64Densiiieafo=
|
||||
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
|
||||
|
|
@ -194,11 +218,14 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf
|
|||
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f h1:hX65Cu3JDlGH3uEdK7I99Ii+9kjD6mvnnpfLdEAH0x4=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
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=
|
||||
|
|
@ -213,6 +240,10 @@ golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73r
|
|||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI=
|
||||
|
|
@ -222,6 +253,8 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 h1:bjcUS9ztw9kFmmIxJInhon/0Is3p+EHBKNgquIzo1OI=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
|
@ -232,18 +265,26 @@ golang.org/x/sys v0.0.0-20181218192612-074acd46bca6/go.mod h1:STP8DvDyc/dI5b8T5h
|
|||
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb h1:fgwFCsaw9buMuxNd6+DQfAuSFqbNiQZpcgJQAgJsK6k=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 h1:+DCIGbF/swA92ohVg0//6X2IVY3KZs6p9mix0ziNYJM=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181219222714-6e267b5cc78e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190213015956-f7e1b50d2251/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138 h1:H3uGjxCR/6Ds0Mjgyp7LMK81+LvmbvWWEnJhzk1Pi9E=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190708203411-c8855242db9c h1:rRFNgkkT7zOyWlroLBmsrKYtBNhox8WtulQlOr3jIDk=
|
||||
golang.org/x/tools v0.0.0-20190708203411-c8855242db9c/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
|
||||
google.golang.org/api v0.0.0-20181220000619-583d854617af/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.2.0 h1:B5VXkdjt7K2Gm6fGBC9C9a1OAKJDT95cTqwet+2zib0=
|
||||
google.golang.org/api v0.2.0/go.mod h1:IfRCZScioGtypHNTlz3gFk67J8uePVW7uDTBzXuIkhU=
|
||||
|
|
@ -262,12 +303,15 @@ google.golang.org/grpc v1.19.1 h1:TrBcJ1yqAl1G++wO39nD/qtgpsW9/1+QGrluyMGEYgM=
|
|||
google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
|
@ -296,6 +340,7 @@ sigs.k8s.io/controller-runtime v0.1.10 h1:amLOmcekVdnsD1uIpmgRqfTbQWJ2qxvQkcdeFh
|
|||
sigs.k8s.io/controller-runtime v0.1.10/go.mod h1:HFAYoOh6XMV+jKF1UjFwrknPbowfyHEHHRdJMf2jMX8=
|
||||
sigs.k8s.io/controller-tools v0.1.11-0.20190411181648-9d55346c2bde h1:ZkaHf5rNYzIB6CB82keKMQNv7xxkqT0ylOBdfJPfi+k=
|
||||
sigs.k8s.io/controller-tools v0.1.11-0.20190411181648-9d55346c2bde/go.mod h1:ATWLRP3WGxuAN9HcT2LaKHReXIH+EZGzRuMHuxjXfhQ=
|
||||
sigs.k8s.io/testing_frameworks v0.1.0 h1:2hBE1sDhKWALoqvhi2i/mnQOFZVfWtQFtsfH0QBTI0U=
|
||||
sigs.k8s.io/testing_frameworks v0.1.0/go.mod h1:VVBKrHmJ6Ekkfz284YKhQePcdycOzNH9qL6ht1zEr/U=
|
||||
sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
|
||||
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
package notifier
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
"time"
|
||||
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
|
||||
|
||||
"github.com/mailgun/mailgun-go/v3"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
)
|
||||
|
||||
const content = `
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html>
|
||||
<head></head>
|
||||
<body>
|
||||
<h1 style="background-color: %s; color: white; padding: 3px 10px;">Jenkins Operator Reconciled</h1>
|
||||
<h3>Failed to do something</h3>
|
||||
<table>
|
||||
<tr>
|
||||
<td><b>CR name:</b></td>
|
||||
<td>%s</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Configuration type:</b></td>
|
||||
<td>%s</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Status:</b></td>
|
||||
<td><b style="color: %s;">%s</b></td>
|
||||
</tr>
|
||||
</table>
|
||||
<h6 style="font-size: 11px; color: grey; margin-top: 15px;">Powered by Jenkins Operator <3</h6>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
// Mailgun is service for sending emails
|
||||
type Mailgun struct{}
|
||||
|
||||
// Send is function for sending directly to API
|
||||
func (m Mailgun) Send(n *Notification, config v1alpha2.Notification) error {
|
||||
secret := &corev1.Secret{}
|
||||
i := n.Information
|
||||
|
||||
selector := config.Mailgun.APIKeySecretKeySelector
|
||||
|
||||
err := n.K8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: n.Jenkins.Namespace}, secret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
secretValue := string(secret.Data[selector.Name])
|
||||
if secretValue == "" {
|
||||
return errors.Errorf("SecretValue %s is empty", selector.Name)
|
||||
}
|
||||
|
||||
mg := mailgun.NewMailgun(config.Mailgun.Domain, secretValue)
|
||||
|
||||
htmlMessage := fmt.Sprintf(content, getStatusColor(i.LogLevel, m), i.CrName, i.ConfigurationType, getStatusColor(i.LogLevel, m), string(i.LogLevel))
|
||||
|
||||
msg := mg.NewMessage(fmt.Sprintf("Jenkins Operator Notifier <%s>", config.Mailgun.From), "Jenkins Operator Status", "", config.Mailgun.Recipient)
|
||||
msg.SetHtml(htmlMessage)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
_, _, err = mg.Send(ctx, msg)
|
||||
|
||||
return err
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
package notifier
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/pkg/errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
)
|
||||
|
||||
// Teams is Microsoft Teams Service
|
||||
type Teams struct{}
|
||||
|
||||
// TeamsMessage is representation of json message structure
|
||||
type TeamsMessage struct {
|
||||
Type string `json:"@type"`
|
||||
Context string `json:"@context"`
|
||||
ThemeColor StatusColor `json:"themeColor"`
|
||||
Title string `json:"title"`
|
||||
Sections []TeamsSection `json:"sections"`
|
||||
}
|
||||
|
||||
// TeamsSection is MS Teams message section
|
||||
type TeamsSection struct {
|
||||
Facts []TeamsFact `json:"facts"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
// TeamsFact is field where we can put content
|
||||
type TeamsFact struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// Send is function for sending directly to API
|
||||
func (t Teams) Send(n *Notification, config v1alpha2.Notification) error {
|
||||
secret := &corev1.Secret{}
|
||||
i := n.Information
|
||||
|
||||
selector := config.Teams.URLSecretKeySelector
|
||||
|
||||
err := n.K8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: n.Jenkins.Namespace}, secret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg, err := json.Marshal(TeamsMessage{
|
||||
Type: "MessageCard",
|
||||
Context: "https://schema.org/extensions",
|
||||
ThemeColor: getStatusColor(i.LogLevel, t),
|
||||
Title: titleText,
|
||||
Sections: []TeamsSection{
|
||||
{
|
||||
Facts: []TeamsFact{
|
||||
{
|
||||
Name: crNameFieldName,
|
||||
Value: i.CrName,
|
||||
},
|
||||
{
|
||||
Name: configurationTypeFieldName,
|
||||
Value: i.ConfigurationType,
|
||||
},
|
||||
{
|
||||
Name: loggingLevelFieldName,
|
||||
Value: string(i.LogLevel),
|
||||
},
|
||||
{
|
||||
Name: namespaceFieldName,
|
||||
Value: i.Namespace,
|
||||
},
|
||||
},
|
||||
Text: i.Message,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
secretValue := string(secret.Data[selector.Key])
|
||||
if secretValue == "" {
|
||||
return errors.Errorf("SecretValue %s is empty", selector.Name)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
request, err := http.NewRequest("POST", secretValue, bytes.NewBuffer(msg))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := client.Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
package notifier
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
)
|
||||
|
||||
func TestTeams_Send(t *testing.T) {
|
||||
fakeClient := fake.NewFakeClient()
|
||||
testURLSelectorKeyName := "test-url-selector"
|
||||
testSecretName := "test-secret"
|
||||
|
||||
i := Information{
|
||||
ConfigurationType: testConfigurationType,
|
||||
CrName: testCrName,
|
||||
Message: testMessage,
|
||||
MessageVerbose: testMessageVerbose,
|
||||
Namespace: testNamespace,
|
||||
LogLevel: testLoggingLevel,
|
||||
}
|
||||
|
||||
notification := &Notification{
|
||||
K8sClient: fakeClient,
|
||||
Information: i,
|
||||
}
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var message TeamsMessage
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
err := decoder.Decode(&message)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, message.Title, titleText)
|
||||
assert.Equal(t, message.ThemeColor, getStatusColor(i.LogLevel, Teams{}))
|
||||
|
||||
mainSection := message.Sections[0]
|
||||
|
||||
assert.Equal(t, mainSection.Text, i.Message)
|
||||
|
||||
for _, fact := range mainSection.Facts {
|
||||
switch fact.Name {
|
||||
case configurationTypeFieldName:
|
||||
assert.Equal(t, fact.Value, i.ConfigurationType)
|
||||
case crNameFieldName:
|
||||
assert.Equal(t, fact.Value, i.CrName)
|
||||
case messageFieldName:
|
||||
assert.Equal(t, fact.Value, i.Message)
|
||||
case loggingLevelFieldName:
|
||||
assert.Equal(t, fact.Value, string(i.LogLevel))
|
||||
case namespaceFieldName:
|
||||
assert.Equal(t, fact.Value, i.Namespace)
|
||||
default:
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
teams := Teams{}
|
||||
|
||||
defer server.Close()
|
||||
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: testSecretName,
|
||||
},
|
||||
|
||||
Data: map[string][]byte{
|
||||
testURLSelectorKeyName: []byte(server.URL),
|
||||
},
|
||||
}
|
||||
|
||||
err := notification.K8sClient.Create(context.TODO(), secret)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = teams.Send(notification, v1alpha2.Notification{
|
||||
Teams: v1alpha2.Teams{
|
||||
URLSecretKeySelector: v1alpha2.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: testSecretName,
|
||||
},
|
||||
Key: testURLSelectorKeyName,
|
||||
},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
package notifier
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/log"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
const (
|
||||
// LogWarn is warning log entry
|
||||
LogWarn LoggingLevel = "warn"
|
||||
|
||||
// LogInfo is info log entry
|
||||
LogInfo LoggingLevel = "info"
|
||||
|
||||
titleText = "Operator reconciled."
|
||||
messageFieldName = "Message"
|
||||
loggingLevelFieldName = "Logging Level"
|
||||
crNameFieldName = "CR Name"
|
||||
configurationTypeFieldName = "Configuration Type"
|
||||
namespaceFieldName = "Namespace"
|
||||
footerContent = "Powered by Jenkins Operator <3"
|
||||
)
|
||||
|
||||
var (
|
||||
testConfigurationType = "test-configuration"
|
||||
testCrName = "test-cr"
|
||||
testNamespace = "test-namespace"
|
||||
testMessage = "test-message"
|
||||
testMessageVerbose = "detail-test-message"
|
||||
testLoggingLevel = LogWarn
|
||||
|
||||
client = http.Client{}
|
||||
)
|
||||
|
||||
// StatusColor is useful for better UX
|
||||
type StatusColor string
|
||||
|
||||
// LoggingLevel is type for selecting different logging levels
|
||||
type LoggingLevel string
|
||||
|
||||
// Information represents details about operator status
|
||||
type Information struct {
|
||||
ConfigurationType string
|
||||
Namespace string
|
||||
CrName string
|
||||
LogLevel LoggingLevel
|
||||
Message string
|
||||
MessageVerbose string
|
||||
}
|
||||
|
||||
// Notification contains message which will be sent
|
||||
type Notification struct {
|
||||
Jenkins v1alpha2.Jenkins
|
||||
K8sClient k8sclient.Client
|
||||
Logger logr.Logger
|
||||
Information Information
|
||||
}
|
||||
|
||||
// Service is skeleton for additional services
|
||||
type service interface {
|
||||
Send(i *Notification, config v1alpha2.Notification) error
|
||||
}
|
||||
|
||||
// Listen is goroutine that listens for incoming messages and sends it
|
||||
func Listen(notification chan *Notification) {
|
||||
for n := range notification {
|
||||
for _, notificationConfig := range n.Jenkins.Spec.Notifications {
|
||||
var err error
|
||||
var svc service
|
||||
|
||||
if notificationConfig.Slack != (v1alpha2.Slack{}) {
|
||||
svc = Slack{}
|
||||
} else if notificationConfig.Teams != (v1alpha2.Teams{}) {
|
||||
svc = Teams{}
|
||||
} else if notificationConfig.Mailgun != (v1alpha2.Mailgun{}) {
|
||||
svc = Mailgun{}
|
||||
} else {
|
||||
n.Logger.V(log.VWarn).Info(fmt.Sprintf("Notification service in `%s` not found or not defined", notificationConfig.Name))
|
||||
continue
|
||||
}
|
||||
|
||||
err = notify(svc, n, notificationConfig)
|
||||
|
||||
if err != nil {
|
||||
n.Logger.V(log.VWarn).Info(fmt.Sprintf("Failed to send notifications. %+v", err))
|
||||
} else {
|
||||
n.Logger.V(log.VDebug).Info("Sent notification")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func getStatusColor(logLevel LoggingLevel, svc service) StatusColor {
|
||||
switch svc.(type) {
|
||||
case Slack:
|
||||
switch logLevel {
|
||||
case LogInfo:
|
||||
return "#439FE0"
|
||||
case LogWarn:
|
||||
return "danger"
|
||||
default:
|
||||
return "#c8c8c8"
|
||||
}
|
||||
case Teams:
|
||||
switch logLevel {
|
||||
case LogInfo:
|
||||
return "439FE0"
|
||||
case LogWarn:
|
||||
return "E81123"
|
||||
default:
|
||||
return "C8C8C8"
|
||||
}
|
||||
case Mailgun:
|
||||
switch logLevel {
|
||||
case LogInfo:
|
||||
return "blue"
|
||||
case LogWarn:
|
||||
return "red"
|
||||
default:
|
||||
return "gray"
|
||||
}
|
||||
default:
|
||||
return "#c8c8c8"
|
||||
}
|
||||
}
|
||||
|
||||
func notify(svc service, n *Notification, manifest v1alpha2.Notification) error {
|
||||
if n.Information.LogLevel == LogInfo && string(manifest.LoggingLevel) == string(LogWarn) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return svc.Send(n, manifest)
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
package notifier
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
)
|
||||
|
||||
// Slack is messaging service
|
||||
type Slack struct{}
|
||||
|
||||
// SlackMessage is representation of json message
|
||||
type SlackMessage struct {
|
||||
Text string `json:"text"`
|
||||
Attachments []SlackAttachment `json:"attachments"`
|
||||
}
|
||||
|
||||
// SlackAttachment is representation of json attachment
|
||||
type SlackAttachment struct {
|
||||
Fallback string `json:"fallback"`
|
||||
Color StatusColor `json:"color"`
|
||||
Pretext string `json:"pretext"`
|
||||
Title string `json:"title"`
|
||||
Text string `json:"text"`
|
||||
Fields []SlackField `json:"fields"`
|
||||
Footer string `json:"footer"`
|
||||
}
|
||||
|
||||
// SlackField is representation of json field.
|
||||
type SlackField struct {
|
||||
Title string `json:"title"`
|
||||
Value string `json:"value"`
|
||||
Short bool `json:"short"`
|
||||
}
|
||||
|
||||
// Send is function for sending directly to API
|
||||
func (s Slack) Send(n *Notification, config v1alpha2.Notification) error {
|
||||
secret := &corev1.Secret{}
|
||||
i := n.Information
|
||||
|
||||
selector := config.Slack.URLSecretKeySelector
|
||||
|
||||
err := n.K8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: n.Jenkins.Namespace}, secret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
slackMessage, err := json.Marshal(SlackMessage{
|
||||
Attachments: []SlackAttachment{
|
||||
{
|
||||
Fallback: "",
|
||||
Color: getStatusColor(i.LogLevel, s),
|
||||
Text: titleText,
|
||||
Fields: []SlackField{
|
||||
{
|
||||
Title: messageFieldName,
|
||||
Value: i.Message,
|
||||
Short: false,
|
||||
},
|
||||
{
|
||||
Title: crNameFieldName,
|
||||
Value: i.CrName,
|
||||
Short: true,
|
||||
},
|
||||
{
|
||||
Title: configurationTypeFieldName,
|
||||
Value: i.ConfigurationType,
|
||||
Short: true,
|
||||
},
|
||||
{
|
||||
Title: loggingLevelFieldName,
|
||||
Value: string(i.LogLevel),
|
||||
Short: true,
|
||||
},
|
||||
{
|
||||
Title: namespaceFieldName,
|
||||
Value: i.Namespace,
|
||||
Short: true,
|
||||
},
|
||||
},
|
||||
Footer: footerContent,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
secretValue := string(secret.Data[selector.Key])
|
||||
if secretValue == "" {
|
||||
return errors.Errorf("SecretValue %s is empty", selector.Name)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
request, err := http.NewRequest("POST", secretValue, bytes.NewBuffer(slackMessage))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := client.Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
package notifier
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
)
|
||||
|
||||
func TestSlack_Send(t *testing.T) {
|
||||
fakeClient := fake.NewFakeClient()
|
||||
testURLSelectorKeyName := "test-url-selector"
|
||||
testSecretName := "test-secret"
|
||||
|
||||
i := Information{
|
||||
ConfigurationType: testConfigurationType,
|
||||
CrName: testCrName,
|
||||
Message: testMessage,
|
||||
MessageVerbose: testMessageVerbose,
|
||||
Namespace: testNamespace,
|
||||
LogLevel: testLoggingLevel,
|
||||
}
|
||||
|
||||
notification := &Notification{
|
||||
K8sClient: fakeClient,
|
||||
Information: i,
|
||||
}
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var message SlackMessage
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
err := decoder.Decode(&message)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
mainAttachment := message.Attachments[0]
|
||||
|
||||
assert.Equal(t, mainAttachment.Text, titleText)
|
||||
for _, field := range mainAttachment.Fields {
|
||||
switch field.Title {
|
||||
case configurationTypeFieldName:
|
||||
assert.Equal(t, field.Value, i.ConfigurationType)
|
||||
case crNameFieldName:
|
||||
assert.Equal(t, field.Value, i.CrName)
|
||||
case messageFieldName:
|
||||
assert.Equal(t, field.Value, i.Message)
|
||||
case loggingLevelFieldName:
|
||||
assert.Equal(t, field.Value, string(i.LogLevel))
|
||||
case namespaceFieldName:
|
||||
assert.Equal(t, field.Value, i.Namespace)
|
||||
default:
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, mainAttachment.Footer, footerContent)
|
||||
assert.Equal(t, mainAttachment.Color, getStatusColor(i.LogLevel, Slack{}))
|
||||
}))
|
||||
|
||||
defer server.Close()
|
||||
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: testSecretName,
|
||||
},
|
||||
|
||||
Data: map[string][]byte{
|
||||
testURLSelectorKeyName: []byte(server.URL),
|
||||
},
|
||||
}
|
||||
|
||||
err := notification.K8sClient.Create(context.TODO(), secret)
|
||||
assert.NoError(t, err)
|
||||
|
||||
slack := Slack{}
|
||||
|
||||
err = slack.Send(notification, v1alpha2.Notification{
|
||||
Slack: v1alpha2.Slack{
|
||||
URLSecretKeySelector: v1alpha2.SecretKeySelector{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: testSecretName,
|
||||
},
|
||||
Key: testURLSelectorKeyName,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
|
@ -17,6 +17,10 @@ type JenkinsSpec struct {
|
|||
// +optional
|
||||
SeedJobs []SeedJob `json:"seedJobs,omitempty"`
|
||||
|
||||
// Notifications defines services which are used to inform about Jenkins status
|
||||
// Can be used to integrate chat services like Slack or Email services like Mailgun
|
||||
Notifications []Notification `json:"notifications,omitempty"`
|
||||
|
||||
// Service is Kubernetes service of Jenkins master HTTP pod
|
||||
// Defaults to :
|
||||
// port: 8080
|
||||
|
|
@ -40,6 +44,52 @@ type JenkinsSpec struct {
|
|||
// More info: https://github.com/jenkinsci/kubernetes-operator/blob/master/docs/getting-started.md#configure-backup-and-restore
|
||||
// +optional
|
||||
Restore Restore `json:"restore,omitempty"`
|
||||
|
||||
// GroovyScripts defines configuration of Jenkins customization via groovy scripts
|
||||
// +optional
|
||||
GroovyScripts GroovyScripts `json:"groovyScripts,omitempty"`
|
||||
|
||||
// ConfigurationAsCode defines configuration of Jenkins customization via Configuration as Code Jenkins plugin
|
||||
// +optional
|
||||
ConfigurationAsCode ConfigurationAsCode `json:"configurationAsCode,omitempty"`
|
||||
}
|
||||
|
||||
// Notification is info sending service about Jenkins Operator
|
||||
type Notification struct {
|
||||
LoggingLevel JenkinsNotificationLogLevel `json:"loggingLevel"`
|
||||
Verbose bool `json:"verbose"`
|
||||
Name string `json:"name"`
|
||||
Slack Slack `json:"slack,omitempty"`
|
||||
Teams Teams `json:"teams,omitempty"`
|
||||
Mailgun Mailgun `json:"mailgun,omitempty"`
|
||||
}
|
||||
|
||||
// Slack is handler for Slack
|
||||
type Slack struct {
|
||||
// The web hook URL to Slack App
|
||||
URLSecretKeySelector SecretKeySelector `json:"urlSecretKeySelector"`
|
||||
}
|
||||
|
||||
// Teams is handler for Microsoft Teams
|
||||
type Teams struct {
|
||||
// The web hook URL to Teams App
|
||||
URLSecretKeySelector SecretKeySelector `json:"urlSecretKeySelector"`
|
||||
}
|
||||
|
||||
// Mailgun is handler for Mailgun email service
|
||||
type Mailgun struct {
|
||||
Domain string `json:"domain"`
|
||||
APIKeySecretKeySelector SecretKeySelector `json:"apiKeySecretKeySelector"`
|
||||
Recipient string `json:"recipient"`
|
||||
From string `json:"from"`
|
||||
}
|
||||
|
||||
// SecretKeySelector selects a key of a Secret.
|
||||
type SecretKeySelector struct {
|
||||
// The name of the secret in the pod's namespace to select from.
|
||||
corev1.LocalObjectReference `json:",inline" protobuf:"bytes,1,opt,name=localObjectReference"`
|
||||
// The key of the secret to select from. Must be a valid secret key.
|
||||
Key string `json:"key" protobuf:"bytes,2,opt,name=key"`
|
||||
}
|
||||
|
||||
// Container defines Kubernetes container attributes
|
||||
|
|
@ -202,6 +252,13 @@ type JenkinsMaster struct {
|
|||
// memory: 600Mi
|
||||
Containers []Container `json:"containers,omitempty"`
|
||||
|
||||
// ImagePullSecrets is an optional list of references to secrets in the same namespace to use for pulling any of the images used by this PodSpec.
|
||||
// If specified, these secrets will be passed to individual puller implementations for them to use. For example,
|
||||
// in the case of docker, only DockerConfig type secrets are honored.
|
||||
// More info: https://kubernetes.io/docs/concepts/containers/images#specifying-imagepullsecrets-on-a-pod
|
||||
// +optional
|
||||
ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"`
|
||||
|
||||
// List of volumes that can be mounted by containers belonging to the pod.
|
||||
// More info: https://kubernetes.io/docs/concepts/storage/volumes
|
||||
// +optional
|
||||
|
|
@ -313,7 +370,7 @@ type JenkinsStatus struct {
|
|||
// +optional
|
||||
UserConfigurationCompletedTime *metav1.Time `json:"userConfigurationCompletedTime,omitempty"`
|
||||
|
||||
// Builds contains Jenkins builds statues
|
||||
// Builds contains Jenkins job builds statues
|
||||
// +optional
|
||||
Builds []Build `json:"builds,omitempty"`
|
||||
|
||||
|
|
@ -340,6 +397,10 @@ type JenkinsStatus struct {
|
|||
// CreatedSeedJobs contains list of seed job id already created in Jenkins
|
||||
// +optional
|
||||
CreatedSeedJobs []string `json:"createdSeedJobs,omitempty"`
|
||||
|
||||
// AppliedGroovyScripts is a list with all applied groovy scripts in Jenkins by the operator
|
||||
// +optional
|
||||
AppliedGroovyScripts []AppliedGroovyScript `json:"appliedGroovyScripts,omitempty"`
|
||||
}
|
||||
|
||||
// BuildStatus defines type of Jenkins build job status
|
||||
|
|
@ -362,9 +423,8 @@ const (
|
|||
BuildExpiredStatus BuildStatus = "expired"
|
||||
)
|
||||
|
||||
// Build defines Jenkins Build status with corresponding metadata
|
||||
// Build defines Jenkins job build status with corresponding metadata
|
||||
type Build struct {
|
||||
|
||||
// JobName is the Jenkins job name
|
||||
JobName string `json:"jobName,omitempty"`
|
||||
|
||||
|
|
@ -425,6 +485,17 @@ const (
|
|||
UsernamePasswordCredentialType JenkinsCredentialType = "usernamePassword"
|
||||
)
|
||||
|
||||
// JenkinsNotificationLogLevel defines type of Notification feature frequency of sending logger entries
|
||||
type JenkinsNotificationLogLevel string
|
||||
|
||||
const (
|
||||
// LogLevelWarning - Only Warnings
|
||||
LogLevelWarning JenkinsNotificationLogLevel = "warning"
|
||||
|
||||
// LogLevelInfo - Only info
|
||||
LogLevelInfo JenkinsNotificationLogLevel = "info"
|
||||
)
|
||||
|
||||
// AllowedJenkinsCredentialMap contains all allowed Jenkins credentials types
|
||||
var AllowedJenkinsCredentialMap = map[string]string{
|
||||
string(NoJenkinsCredentialCredentialType): "",
|
||||
|
|
@ -493,3 +564,41 @@ type Restore struct {
|
|||
// +optional
|
||||
RecoveryOnce uint64 `json:"recoveryOnce,omitempty"`
|
||||
}
|
||||
|
||||
// AppliedGroovyScript is the applied groovy script in Jenkins by the operator
|
||||
type AppliedGroovyScript struct {
|
||||
// ConfigurationType is the name of the configuration type(base-groovy, user-groovy, user-casc)
|
||||
ConfigurationType string `json:"configurationType"`
|
||||
// Source is the name of source where is located groovy script
|
||||
Source string `json:"source"`
|
||||
// Name is the name of the groovy script
|
||||
Name string `json:"name"`
|
||||
// Hash is the hash of the groovy script and secrets which it uses
|
||||
Hash string
|
||||
}
|
||||
|
||||
// SecretRef is reference to Kubernetes secret
|
||||
type SecretRef struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// ConfigMapRef is reference to Kubernetes ConfigMap
|
||||
type ConfigMapRef struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// Customization defines configuration of Jenkins customization
|
||||
type Customization struct {
|
||||
Secret SecretRef `json:"secret"`
|
||||
Configurations []ConfigMapRef `json:"configurations"`
|
||||
}
|
||||
|
||||
// GroovyScripts defines configuration of Jenkins customization via groovy scripts
|
||||
type GroovyScripts struct {
|
||||
Customization
|
||||
}
|
||||
|
||||
// ConfigurationAsCode defines configuration of Jenkins customization via Configuration as Code Jenkins plugin
|
||||
type ConfigurationAsCode struct {
|
||||
Customization
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ func (in *Jenkins) GroupVersionKind() schema.GroupVersionKind {
|
|||
}
|
||||
}
|
||||
|
||||
// JenkinsTypeMeta returns Jenkins type meta
|
||||
func JenkinsTypeMeta() metav1.TypeMeta {
|
||||
return metav1.TypeMeta{
|
||||
Kind: Kind,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,22 @@ import (
|
|||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *AppliedGroovyScript) DeepCopyInto(out *AppliedGroovyScript) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppliedGroovyScript.
|
||||
func (in *AppliedGroovyScript) DeepCopy() *AppliedGroovyScript {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(AppliedGroovyScript)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Backup) DeepCopyInto(out *Backup) {
|
||||
*out = *in
|
||||
|
|
@ -50,6 +66,39 @@ func (in *Build) DeepCopy() *Build {
|
|||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ConfigMapRef) DeepCopyInto(out *ConfigMapRef) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigMapRef.
|
||||
func (in *ConfigMapRef) DeepCopy() *ConfigMapRef {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ConfigMapRef)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ConfigurationAsCode) DeepCopyInto(out *ConfigurationAsCode) {
|
||||
*out = *in
|
||||
in.Customization.DeepCopyInto(&out.Customization)
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigurationAsCode.
|
||||
func (in *ConfigurationAsCode) DeepCopy() *ConfigurationAsCode {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ConfigurationAsCode)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Container) DeepCopyInto(out *Container) {
|
||||
*out = *in
|
||||
|
|
@ -123,6 +172,45 @@ func (in *Container) DeepCopy() *Container {
|
|||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Customization) DeepCopyInto(out *Customization) {
|
||||
*out = *in
|
||||
out.Secret = in.Secret
|
||||
if in.Configurations != nil {
|
||||
in, out := &in.Configurations, &out.Configurations
|
||||
*out = make([]ConfigMapRef, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Customization.
|
||||
func (in *Customization) DeepCopy() *Customization {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Customization)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *GroovyScripts) DeepCopyInto(out *GroovyScripts) {
|
||||
*out = *in
|
||||
in.Customization.DeepCopyInto(&out.Customization)
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GroovyScripts.
|
||||
func (in *GroovyScripts) DeepCopy() *GroovyScripts {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(GroovyScripts)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Handler) DeepCopyInto(out *Handler) {
|
||||
*out = *in
|
||||
|
|
@ -234,6 +322,11 @@ func (in *JenkinsMaster) DeepCopyInto(out *JenkinsMaster) {
|
|||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.ImagePullSecrets != nil {
|
||||
in, out := &in.ImagePullSecrets, &out.ImagePullSecrets
|
||||
*out = make([]v1.LocalObjectReference, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Volumes != nil {
|
||||
in, out := &in.Volumes, &out.Volumes
|
||||
*out = make([]v1.Volume, len(*in))
|
||||
|
|
@ -273,10 +366,17 @@ func (in *JenkinsSpec) DeepCopyInto(out *JenkinsSpec) {
|
|||
*out = make([]SeedJob, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Notifications != nil {
|
||||
in, out := &in.Notifications, &out.Notifications
|
||||
*out = make([]Notification, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
in.Service.DeepCopyInto(&out.Service)
|
||||
in.SlaveService.DeepCopyInto(&out.SlaveService)
|
||||
in.Backup.DeepCopyInto(&out.Backup)
|
||||
in.Restore.DeepCopyInto(&out.Restore)
|
||||
in.GroovyScripts.DeepCopyInto(&out.GroovyScripts)
|
||||
in.ConfigurationAsCode.DeepCopyInto(&out.ConfigurationAsCode)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -317,6 +417,11 @@ func (in *JenkinsStatus) DeepCopyInto(out *JenkinsStatus) {
|
|||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.AppliedGroovyScripts != nil {
|
||||
in, out := &in.AppliedGroovyScripts, &out.AppliedGroovyScripts
|
||||
*out = make([]AppliedGroovyScript, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -330,6 +435,42 @@ func (in *JenkinsStatus) DeepCopy() *JenkinsStatus {
|
|||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Mailgun) DeepCopyInto(out *Mailgun) {
|
||||
*out = *in
|
||||
out.APIKeySecretKeySelector = in.APIKeySecretKeySelector
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Mailgun.
|
||||
func (in *Mailgun) DeepCopy() *Mailgun {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Mailgun)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Notification) DeepCopyInto(out *Notification) {
|
||||
*out = *in
|
||||
out.Slack = in.Slack
|
||||
out.Teams = in.Teams
|
||||
out.Mailgun = in.Mailgun
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Notification.
|
||||
func (in *Notification) DeepCopy() *Notification {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Notification)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Plugin) DeepCopyInto(out *Plugin) {
|
||||
*out = *in
|
||||
|
|
@ -363,6 +504,39 @@ func (in *Restore) DeepCopy() *Restore {
|
|||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SecretKeySelector) DeepCopyInto(out *SecretKeySelector) {
|
||||
*out = *in
|
||||
out.LocalObjectReference = in.LocalObjectReference
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretKeySelector.
|
||||
func (in *SecretKeySelector) DeepCopy() *SecretKeySelector {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(SecretKeySelector)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SecretRef) DeepCopyInto(out *SecretRef) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretRef.
|
||||
func (in *SecretRef) DeepCopy() *SecretRef {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(SecretRef)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SeedJob) DeepCopyInto(out *SeedJob) {
|
||||
*out = *in
|
||||
|
|
@ -413,3 +587,37 @@ func (in *Service) DeepCopy() *Service {
|
|||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Slack) DeepCopyInto(out *Slack) {
|
||||
*out = *in
|
||||
out.URLSecretKeySelector = in.URLSecretKeySelector
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Slack.
|
||||
func (in *Slack) DeepCopy() *Slack {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Slack)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Teams) DeepCopyInto(out *Teams) {
|
||||
*out = *in
|
||||
out.URLSecretKeySelector = in.URLSecretKeySelector
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Teams.
|
||||
func (in *Teams) DeepCopy() *Teams {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Teams)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ func BuildJenkinsAPIUrl(namespace, serviceName string, portNumber int32, local,
|
|||
}
|
||||
|
||||
// Connect through Kubernetes service, operator has to be run inside cluster
|
||||
return fmt.Sprintf("http://%s:%d", serviceName, portNumber), nil
|
||||
return fmt.Sprintf("http://%s.%s:%d", serviceName, namespace, portNumber), nil
|
||||
}
|
||||
|
||||
// New creates Jenkins API client
|
||||
|
|
|
|||
|
|
@ -10,6 +10,13 @@ import (
|
|||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// GroovyScriptExecutionFailed is custom error type which indicates passed groovy script is invalid
|
||||
type GroovyScriptExecutionFailed struct{}
|
||||
|
||||
func (e GroovyScriptExecutionFailed) Error() string {
|
||||
return "script execution failed"
|
||||
}
|
||||
|
||||
func (jenkins *jenkins) ExecuteScript(script string) (string, error) {
|
||||
now := time.Now().Unix()
|
||||
verifier := fmt.Sprintf("verifier-%d", now)
|
||||
|
|
@ -38,7 +45,7 @@ func (jenkins *jenkins) executeScript(script string, verifier string) (string, e
|
|||
}
|
||||
|
||||
if !strings.Contains(output, verifier) {
|
||||
return output, errors.Errorf("script execution failed, logs '%s'", output)
|
||||
return output, &GroovyScriptExecutionFailed{}
|
||||
}
|
||||
|
||||
return output, nil
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ func Test_ExecuteScript(t *testing.T) {
|
|||
|
||||
script := "some groovy code"
|
||||
logs, err := jenkinsClient.executeScript(script, verifier)
|
||||
assert.EqualError(t, err, "script execution failed, logs 'some exception stack trace without verifier'", logs)
|
||||
assert.EqualError(t, err, "script execution failed", logs)
|
||||
assert.Equal(t, response, logs)
|
||||
})
|
||||
t.Run("throw 500", func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import (
|
|||
jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client"
|
||||
"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/constants"
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/groovy"
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/plugins"
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/log"
|
||||
|
|
@ -128,6 +127,32 @@ func (r *ReconcileJenkinsBaseConfiguration) Reconcile() (reconcile.Result, jenki
|
|||
return result, jenkinsClient, err
|
||||
}
|
||||
|
||||
// GetJenkinsOpts put container JENKINS_OPTS env parameters in map and returns it
|
||||
func GetJenkinsOpts(jenkins *v1alpha2.Jenkins) map[string]string {
|
||||
envs := jenkins.Spec.Master.Containers[0].Env
|
||||
jenkinsOpts := make(map[string]string)
|
||||
|
||||
for k, v := range envs {
|
||||
if v.Name == "JENKINS_OPTS" {
|
||||
jenkinsOptsEnv := envs[k]
|
||||
jenkinsOptsWithDashes := jenkinsOptsEnv.Value
|
||||
if len(jenkinsOptsWithDashes) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
jenkinsOptsWithEqOperators := strings.Split(jenkinsOptsWithDashes, " ")
|
||||
|
||||
for _, vx := range jenkinsOptsWithEqOperators {
|
||||
opt := strings.Split(vx, "=")
|
||||
jenkinsOpts[strings.ReplaceAll(opt[0], "--", "")] = opt[1]
|
||||
}
|
||||
|
||||
return jenkinsOpts
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ReconcileJenkinsBaseConfiguration) ensureResourcesRequiredForJenkinsPod(metaObject metav1.ObjectMeta) error {
|
||||
if err := r.createOperatorCredentialsSecret(metaObject); err != nil {
|
||||
return err
|
||||
|
|
@ -149,15 +174,15 @@ func (r *ReconcileJenkinsBaseConfiguration) ensureResourcesRequiredForJenkinsPod
|
|||
}
|
||||
r.logger.V(log.VDebug).Info("Base configuration config map is present")
|
||||
|
||||
if err := r.createUserConfigurationConfigMap(metaObject); err != nil {
|
||||
if err := r.addLabelForWatchesResources(r.jenkins.Spec.GroovyScripts.Customization); err != nil {
|
||||
return err
|
||||
}
|
||||
r.logger.V(log.VDebug).Info("User configuration config map is present")
|
||||
r.logger.V(log.VDebug).Info("GroovyScripts Secret and ConfigMap added watched labels")
|
||||
|
||||
if err := r.createUserConfigurationSecret(metaObject); err != nil {
|
||||
if err := r.addLabelForWatchesResources(r.jenkins.Spec.ConfigurationAsCode.Customization); err != nil {
|
||||
return err
|
||||
}
|
||||
r.logger.V(log.VDebug).Info("User configuration secret is present")
|
||||
r.logger.V(log.VDebug).Info("ConfigurationAsCode Secret and ConfigMap added watched labels")
|
||||
|
||||
if err := r.createRBAC(metaObject); err != nil {
|
||||
return err
|
||||
|
|
@ -289,33 +314,49 @@ func (r *ReconcileJenkinsBaseConfiguration) createBaseConfigurationConfigMap(met
|
|||
return stackerr.WithStack(r.createOrUpdateResource(configMap))
|
||||
}
|
||||
|
||||
func (r *ReconcileJenkinsBaseConfiguration) createUserConfigurationConfigMap(meta metav1.ObjectMeta) error {
|
||||
currentConfigMap := &corev1.ConfigMap{}
|
||||
err := r.k8sClient.Get(context.TODO(), types.NamespacedName{Name: resources.GetUserConfigurationConfigMapNameFromJenkins(r.jenkins), Namespace: r.jenkins.Namespace}, currentConfigMap)
|
||||
if err != nil && errors.IsNotFound(err) {
|
||||
return stackerr.WithStack(r.k8sClient.Create(context.TODO(), resources.NewUserConfigurationConfigMap(r.jenkins)))
|
||||
} else if err != nil {
|
||||
return stackerr.WithStack(err)
|
||||
}
|
||||
if !resources.VerifyIfLabelsAreSet(currentConfigMap, resources.BuildLabelsForWatchedResources(*r.jenkins)) {
|
||||
currentConfigMap.ObjectMeta.Labels = resources.BuildLabelsForWatchedResources(*r.jenkins)
|
||||
return stackerr.WithStack(r.k8sClient.Update(context.TODO(), currentConfigMap))
|
||||
func (r *ReconcileJenkinsBaseConfiguration) addLabelForWatchesResources(customization v1alpha2.Customization) error {
|
||||
labelsForWatchedResources := resources.BuildLabelsForWatchedResources(*r.jenkins)
|
||||
|
||||
if len(customization.Secret.Name) > 0 {
|
||||
secret := &corev1.Secret{}
|
||||
err := r.k8sClient.Get(context.TODO(), types.NamespacedName{Name: customization.Secret.Name, Namespace: r.jenkins.Namespace}, secret)
|
||||
if err != nil {
|
||||
return stackerr.WithStack(err)
|
||||
}
|
||||
|
||||
if !resources.VerifyIfLabelsAreSet(secret, labelsForWatchedResources) {
|
||||
if len(secret.ObjectMeta.Labels) == 0 {
|
||||
secret.ObjectMeta.Labels = map[string]string{}
|
||||
}
|
||||
for key, value := range labelsForWatchedResources {
|
||||
secret.ObjectMeta.Labels[key] = value
|
||||
}
|
||||
|
||||
if err = r.k8sClient.Update(context.TODO(), secret); err != nil {
|
||||
return stackerr.WithStack(r.k8sClient.Update(context.TODO(), secret))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
for _, configMapRef := range customization.Configurations {
|
||||
configMap := &corev1.ConfigMap{}
|
||||
err := r.k8sClient.Get(context.TODO(), types.NamespacedName{Name: configMapRef.Name, Namespace: r.jenkins.Namespace}, configMap)
|
||||
if err != nil {
|
||||
return stackerr.WithStack(err)
|
||||
}
|
||||
|
||||
func (r *ReconcileJenkinsBaseConfiguration) createUserConfigurationSecret(meta metav1.ObjectMeta) error {
|
||||
currentSecret := &corev1.Secret{}
|
||||
err := r.k8sClient.Get(context.TODO(), types.NamespacedName{Name: resources.GetUserConfigurationSecretNameFromJenkins(r.jenkins), Namespace: r.jenkins.Namespace}, currentSecret)
|
||||
if err != nil && errors.IsNotFound(err) {
|
||||
return stackerr.WithStack(r.k8sClient.Create(context.TODO(), resources.NewUserConfigurationSecret(r.jenkins)))
|
||||
} else if err != nil {
|
||||
return stackerr.WithStack(err)
|
||||
}
|
||||
if !resources.VerifyIfLabelsAreSet(currentSecret, resources.BuildLabelsForWatchedResources(*r.jenkins)) {
|
||||
currentSecret.ObjectMeta.Labels = resources.BuildLabelsForWatchedResources(*r.jenkins)
|
||||
return stackerr.WithStack(r.k8sClient.Update(context.TODO(), currentSecret))
|
||||
if !resources.VerifyIfLabelsAreSet(configMap, labelsForWatchedResources) {
|
||||
if len(configMap.ObjectMeta.Labels) == 0 {
|
||||
configMap.ObjectMeta.Labels = map[string]string{}
|
||||
}
|
||||
for key, value := range labelsForWatchedResources {
|
||||
configMap.ObjectMeta.Labels[key] = value
|
||||
}
|
||||
|
||||
if err = r.k8sClient.Update(context.TODO(), configMap); err != nil {
|
||||
return stackerr.WithStack(r.k8sClient.Update(context.TODO(), configMap))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -486,6 +527,12 @@ func (r *ReconcileJenkinsBaseConfiguration) isRecreatePodNeeded(currentJenkinsMa
|
|||
return true
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(r.jenkins.Spec.Master.ImagePullSecrets, currentJenkinsMasterPod.Spec.ImagePullSecrets) {
|
||||
r.logger.Info(fmt.Sprintf("Jenkins Pod ImagePullSecrets has changed, actual '%+v' required '%+v', recreating pod",
|
||||
currentJenkinsMasterPod.Spec.ImagePullSecrets, r.jenkins.Spec.Master.ImagePullSecrets))
|
||||
return true
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(r.jenkins.Spec.Master.NodeSelector, currentJenkinsMasterPod.Spec.NodeSelector) {
|
||||
r.logger.Info(fmt.Sprintf("Jenkins pod node selector has changed, actual '%+v' required '%+v', recreating pod",
|
||||
currentJenkinsMasterPod.Spec.NodeSelector, r.jenkins.Spec.Master.NodeSelector))
|
||||
|
|
@ -733,6 +780,11 @@ func (r *ReconcileJenkinsBaseConfiguration) waitForJenkins(meta metav1.ObjectMet
|
|||
func (r *ReconcileJenkinsBaseConfiguration) ensureJenkinsClient(meta metav1.ObjectMeta) (jenkinsclient.Jenkins, error) {
|
||||
jenkinsURL, err := jenkinsclient.BuildJenkinsAPIUrl(
|
||||
r.jenkins.ObjectMeta.Namespace, resources.GetJenkinsHTTPServiceName(r.jenkins), r.jenkins.Spec.Service.Port, r.local, r.minikube)
|
||||
|
||||
if prefix, ok := GetJenkinsOpts(r.jenkins)["prefix"]; ok {
|
||||
jenkinsURL = jenkinsURL + prefix
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -792,27 +844,20 @@ func (r *ReconcileJenkinsBaseConfiguration) ensureJenkinsClient(meta metav1.Obje
|
|||
}
|
||||
|
||||
func (r *ReconcileJenkinsBaseConfiguration) ensureBaseConfiguration(jenkinsClient jenkinsclient.Jenkins) (reconcile.Result, error) {
|
||||
groovyClient := groovy.New(jenkinsClient, r.k8sClient, r.logger, fmt.Sprintf("%s-base-configuration", constants.OperatorName), resources.JenkinsBaseConfigurationVolumePath)
|
||||
|
||||
err := groovyClient.ConfigureJob()
|
||||
if err != nil {
|
||||
return reconcile.Result{}, err
|
||||
customization := v1alpha2.GroovyScripts{
|
||||
Customization: v1alpha2.Customization{
|
||||
Secret: v1alpha2.SecretRef{Name: ""},
|
||||
Configurations: []v1alpha2.ConfigMapRef{{Name: resources.GetBaseConfigurationConfigMapName(r.jenkins)}},
|
||||
},
|
||||
}
|
||||
|
||||
configuration := &corev1.ConfigMap{}
|
||||
namespaceName := types.NamespacedName{Namespace: r.jenkins.Namespace, Name: resources.GetBaseConfigurationConfigMapName(r.jenkins)}
|
||||
err = r.k8sClient.Get(context.TODO(), namespaceName, configuration)
|
||||
if err != nil {
|
||||
return reconcile.Result{}, stackerr.WithStack(err)
|
||||
}
|
||||
groovyClient := groovy.New(jenkinsClient, r.k8sClient, r.logger, r.jenkins, "base-groovy", customization.Customization)
|
||||
|
||||
done, err := groovyClient.Ensure(configuration.Data, r.jenkins)
|
||||
if err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
if !done {
|
||||
return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 10}, nil
|
||||
}
|
||||
requeue, err := groovyClient.Ensure(func(name string) bool {
|
||||
return strings.HasSuffix(name, ".groovy")
|
||||
}, func(groovyScript string) string {
|
||||
return groovyScript
|
||||
})
|
||||
|
||||
return reconcile.Result{}, nil
|
||||
return reconcile.Result{Requeue: requeue}, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,141 @@ import (
|
|||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
func TestGetJenkinsOpts(t *testing.T) {
|
||||
t.Run("JENKINS_OPTS is uninitialized", func(t *testing.T) {
|
||||
jenkins := &v1alpha2.Jenkins{
|
||||
Spec: v1alpha2.JenkinsSpec{
|
||||
Master: v1alpha2.JenkinsMaster{
|
||||
Containers: []v1alpha2.Container{
|
||||
{
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "", Value: ""},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
opts := GetJenkinsOpts(jenkins)
|
||||
assert.Equal(t, 0, len(opts))
|
||||
})
|
||||
|
||||
t.Run("JENKINS_OPTS is empty", func(t *testing.T) {
|
||||
jenkins := &v1alpha2.Jenkins{
|
||||
Spec: v1alpha2.JenkinsSpec{
|
||||
Master: v1alpha2.JenkinsMaster{
|
||||
Containers: []v1alpha2.Container{
|
||||
{
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "JENKINS_OPTS", Value: ""},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
opts := GetJenkinsOpts(jenkins)
|
||||
assert.Equal(t, 0, len(opts))
|
||||
})
|
||||
|
||||
t.Run("JENKINS_OPTS have --prefix argument ", func(t *testing.T) {
|
||||
jenkins := &v1alpha2.Jenkins{
|
||||
Spec: v1alpha2.JenkinsSpec{
|
||||
Master: v1alpha2.JenkinsMaster{
|
||||
Containers: []v1alpha2.Container{
|
||||
{
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "JENKINS_OPTS", Value: "--prefix=/jenkins"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
opts := GetJenkinsOpts(jenkins)
|
||||
|
||||
assert.Equal(t, 1, len(opts))
|
||||
assert.NotContains(t, opts, "httpPort")
|
||||
assert.Contains(t, opts, "prefix")
|
||||
assert.Equal(t, opts["prefix"], "/jenkins")
|
||||
})
|
||||
|
||||
t.Run("JENKINS_OPTS have --prefix and --httpPort argument", func(t *testing.T) {
|
||||
jenkins := &v1alpha2.Jenkins{
|
||||
Spec: v1alpha2.JenkinsSpec{
|
||||
Master: v1alpha2.JenkinsMaster{
|
||||
Containers: []v1alpha2.Container{
|
||||
{
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "JENKINS_OPTS", Value: "--prefix=/jenkins --httpPort=8080"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
opts := GetJenkinsOpts(jenkins)
|
||||
|
||||
assert.Equal(t, 2, len(opts))
|
||||
|
||||
assert.Contains(t, opts, "prefix")
|
||||
assert.Equal(t, opts["prefix"], "/jenkins")
|
||||
|
||||
assert.Contains(t, opts, "httpPort")
|
||||
assert.Equal(t, opts["httpPort"], "8080")
|
||||
})
|
||||
|
||||
t.Run("JENKINS_OPTS have --httpPort argument", func(t *testing.T) {
|
||||
jenkins := &v1alpha2.Jenkins{
|
||||
Spec: v1alpha2.JenkinsSpec{
|
||||
Master: v1alpha2.JenkinsMaster{
|
||||
Containers: []v1alpha2.Container{
|
||||
{
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "JENKINS_OPTS", Value: "--httpPort=8080"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
opts := GetJenkinsOpts(jenkins)
|
||||
|
||||
assert.Equal(t, 1, len(opts))
|
||||
assert.NotContains(t, opts, "prefix")
|
||||
assert.Contains(t, opts, "httpPort")
|
||||
assert.Equal(t, opts["httpPort"], "8080")
|
||||
})
|
||||
|
||||
t.Run("JENKINS_OPTS have --httpPort=--8080 argument", func(t *testing.T) {
|
||||
jenkins := &v1alpha2.Jenkins{
|
||||
Spec: v1alpha2.JenkinsSpec{
|
||||
Master: v1alpha2.JenkinsMaster{
|
||||
Containers: []v1alpha2.Container{
|
||||
{
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "JENKINS_OPTS", Value: "--httpPort=--8080"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
opts := GetJenkinsOpts(jenkins)
|
||||
|
||||
assert.Equal(t, 1, len(opts))
|
||||
assert.NotContains(t, opts, "prefix")
|
||||
assert.Contains(t, opts, "httpPort")
|
||||
assert.Equal(t, opts["httpPort"], "--8080")
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompareContainerVolumeMounts(t *testing.T) {
|
||||
t.Run("happy with service account", func(t *testing.T) {
|
||||
expectedContainer := corev1.Container{
|
||||
|
|
|
|||
|
|
@ -30,19 +30,12 @@ const (
|
|||
jenkinsInitConfigurationVolumeName = "init-configuration"
|
||||
jenkinsInitConfigurationVolumePath = jenkinsPath + "/init-configuration"
|
||||
|
||||
jenkinsBaseConfigurationVolumeName = "base-configuration"
|
||||
// JenkinsBaseConfigurationVolumePath is a path where are groovy scripts used to configure Jenkins
|
||||
// this scripts are provided by jenkins-operator
|
||||
JenkinsBaseConfigurationVolumePath = jenkinsPath + "/base-configuration"
|
||||
|
||||
jenkinsUserConfigurationVolumeName = "user-configuration"
|
||||
// JenkinsUserConfigurationVolumePath is a path where are groovy scripts and CasC configs used to configure Jenkins
|
||||
// this script is provided by user
|
||||
JenkinsUserConfigurationVolumePath = jenkinsPath + "/user-configuration"
|
||||
|
||||
userConfigurationSecretVolumeName = "user-configuration-secrets"
|
||||
// UserConfigurationSecretVolumePath is a path where are secrets used for groovy scripts and CasC configs
|
||||
UserConfigurationSecretVolumePath = jenkinsPath + "/user-configuration-secrets"
|
||||
// GroovyScriptsSecretVolumePath is a path where are groovy scripts used to configure Jenkins
|
||||
// This script is provided by user
|
||||
GroovyScriptsSecretVolumePath = jenkinsPath + "/groovy-scripts-secrets"
|
||||
// ConfigurationAsCodeSecretVolumePath is a path where are CasC configs used to configure Jenkins
|
||||
// This script is provided by user
|
||||
ConfigurationAsCodeSecretVolumePath = jenkinsPath + "/configuration-as-code-secrets"
|
||||
|
||||
httpPortName = "http"
|
||||
slavePortName = "slavelistener"
|
||||
|
|
@ -68,8 +61,8 @@ func GetJenkinsMasterContainerBaseCommand() []string {
|
|||
}
|
||||
|
||||
// GetJenkinsMasterContainerBaseEnvs returns Jenkins master pod envs required by operator
|
||||
func GetJenkinsMasterContainerBaseEnvs() []corev1.EnvVar {
|
||||
return []corev1.EnvVar{
|
||||
func GetJenkinsMasterContainerBaseEnvs(jenkins *v1alpha2.Jenkins) []corev1.EnvVar {
|
||||
envVars := []corev1.EnvVar{
|
||||
{
|
||||
Name: "JENKINS_HOME",
|
||||
Value: jenkinsHomePath,
|
||||
|
|
@ -78,11 +71,16 @@ func GetJenkinsMasterContainerBaseEnvs() []corev1.EnvVar {
|
|||
Name: "JAVA_OPTS",
|
||||
Value: "-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=1 -Djenkins.install.runSetupWizard=false -Djava.awt.headless=true",
|
||||
},
|
||||
{
|
||||
Name: "SECRETS", // https://github.com/jenkinsci/configuration-as-code-plugin/blob/master/demos/kubernetes-secrets/README.md
|
||||
Value: UserConfigurationSecretVolumePath,
|
||||
},
|
||||
}
|
||||
|
||||
if len(jenkins.Spec.ConfigurationAsCode.Secret.Name) > 0 {
|
||||
envVars = append(envVars, corev1.EnvVar{
|
||||
Name: "SECRETS", // https://github.com/jenkinsci/configuration-as-code-plugin/blob/master/demos/kubernetes-secrets/README.md
|
||||
Value: ConfigurationAsCodeSecretVolumePath,
|
||||
})
|
||||
}
|
||||
|
||||
return envVars
|
||||
}
|
||||
|
||||
// GetJenkinsMasterPodBaseVolumes returns Jenkins master pod volumes required by operator
|
||||
|
|
@ -90,7 +88,7 @@ func GetJenkinsMasterPodBaseVolumes(jenkins *v1alpha2.Jenkins) []corev1.Volume {
|
|||
configMapVolumeSourceDefaultMode := corev1.ConfigMapVolumeSourceDefaultMode
|
||||
secretVolumeSourceDefaultMode := corev1.SecretVolumeSourceDefaultMode
|
||||
var scriptsVolumeDefaultMode int32 = 0777
|
||||
return []corev1.Volume{
|
||||
volumes := []corev1.Volume{
|
||||
{
|
||||
Name: JenkinsHomeVolumeName,
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
|
|
@ -119,28 +117,6 @@ func GetJenkinsMasterPodBaseVolumes(jenkins *v1alpha2.Jenkins) []corev1.Volume {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: jenkinsBaseConfigurationVolumeName,
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
ConfigMap: &corev1.ConfigMapVolumeSource{
|
||||
DefaultMode: &configMapVolumeSourceDefaultMode,
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: GetBaseConfigurationConfigMapName(jenkins),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: jenkinsUserConfigurationVolumeName,
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
ConfigMap: &corev1.ConfigMapVolumeSource{
|
||||
DefaultMode: &configMapVolumeSourceDefaultMode,
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: GetUserConfigurationConfigMapNameFromJenkins(jenkins),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: jenkinsOperatorCredentialsVolumeName,
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
|
|
@ -150,21 +126,45 @@ func GetJenkinsMasterPodBaseVolumes(jenkins *v1alpha2.Jenkins) []corev1.Volume {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: userConfigurationSecretVolumeName,
|
||||
}
|
||||
|
||||
if len(jenkins.Spec.GroovyScripts.Secret.Name) > 0 {
|
||||
volumes = append(volumes, corev1.Volume{
|
||||
Name: getGroovyScriptsSecretVolumeName(jenkins),
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
Secret: &corev1.SecretVolumeSource{
|
||||
DefaultMode: &secretVolumeSourceDefaultMode,
|
||||
SecretName: GetUserConfigurationSecretNameFromJenkins(jenkins),
|
||||
SecretName: jenkins.Spec.GroovyScripts.Secret.Name,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
if len(jenkins.Spec.ConfigurationAsCode.Secret.Name) > 0 {
|
||||
volumes = append(volumes, corev1.Volume{
|
||||
Name: getConfigurationAsCodeSecretVolumeName(jenkins),
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
Secret: &corev1.SecretVolumeSource{
|
||||
DefaultMode: &secretVolumeSourceDefaultMode,
|
||||
SecretName: jenkins.Spec.ConfigurationAsCode.Secret.Name,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return volumes
|
||||
}
|
||||
|
||||
func getGroovyScriptsSecretVolumeName(jenkins *v1alpha2.Jenkins) string {
|
||||
return "gs-" + jenkins.Spec.GroovyScripts.Secret.Name
|
||||
}
|
||||
|
||||
func getConfigurationAsCodeSecretVolumeName(jenkins *v1alpha2.Jenkins) string {
|
||||
return "casc-" + jenkins.Spec.GroovyScripts.Secret.Name
|
||||
}
|
||||
|
||||
// GetJenkinsMasterContainerBaseVolumeMounts returns Jenkins master pod volume mounts required by operator
|
||||
func GetJenkinsMasterContainerBaseVolumeMounts() []corev1.VolumeMount {
|
||||
return []corev1.VolumeMount{
|
||||
func GetJenkinsMasterContainerBaseVolumeMounts(jenkins *v1alpha2.Jenkins) []corev1.VolumeMount {
|
||||
volumeMounts := []corev1.VolumeMount{
|
||||
{
|
||||
Name: JenkinsHomeVolumeName,
|
||||
MountPath: jenkinsHomePath,
|
||||
|
|
@ -180,33 +180,35 @@ func GetJenkinsMasterContainerBaseVolumeMounts() []corev1.VolumeMount {
|
|||
MountPath: jenkinsInitConfigurationVolumePath,
|
||||
ReadOnly: true,
|
||||
},
|
||||
{
|
||||
Name: jenkinsBaseConfigurationVolumeName,
|
||||
MountPath: JenkinsBaseConfigurationVolumePath,
|
||||
ReadOnly: true,
|
||||
},
|
||||
{
|
||||
Name: jenkinsUserConfigurationVolumeName,
|
||||
MountPath: JenkinsUserConfigurationVolumePath,
|
||||
ReadOnly: true,
|
||||
},
|
||||
{
|
||||
Name: jenkinsOperatorCredentialsVolumeName,
|
||||
MountPath: jenkinsOperatorCredentialsVolumePath,
|
||||
ReadOnly: true,
|
||||
},
|
||||
{
|
||||
Name: userConfigurationSecretVolumeName,
|
||||
MountPath: UserConfigurationSecretVolumePath,
|
||||
ReadOnly: true,
|
||||
},
|
||||
}
|
||||
|
||||
if len(jenkins.Spec.GroovyScripts.Secret.Name) > 0 {
|
||||
volumeMounts = append(volumeMounts, corev1.VolumeMount{
|
||||
Name: getGroovyScriptsSecretVolumeName(jenkins),
|
||||
MountPath: GroovyScriptsSecretVolumePath,
|
||||
ReadOnly: true,
|
||||
})
|
||||
}
|
||||
if len(jenkins.Spec.ConfigurationAsCode.Secret.Name) > 0 {
|
||||
volumeMounts = append(volumeMounts, corev1.VolumeMount{
|
||||
Name: getConfigurationAsCodeSecretVolumeName(jenkins),
|
||||
MountPath: ConfigurationAsCodeSecretVolumePath,
|
||||
ReadOnly: true,
|
||||
})
|
||||
}
|
||||
|
||||
return volumeMounts
|
||||
}
|
||||
|
||||
// NewJenkinsMasterContainer returns Jenkins master Kubernetes container
|
||||
func NewJenkinsMasterContainer(jenkins *v1alpha2.Jenkins) corev1.Container {
|
||||
jenkinsContainer := jenkins.Spec.Master.Containers[0]
|
||||
envs := GetJenkinsMasterContainerBaseEnvs()
|
||||
envs := GetJenkinsMasterContainerBaseEnvs(jenkins)
|
||||
envs = append(envs, jenkinsContainer.Env...)
|
||||
|
||||
return corev1.Container{
|
||||
|
|
@ -230,7 +232,7 @@ func NewJenkinsMasterContainer(jenkins *v1alpha2.Jenkins) corev1.Container {
|
|||
},
|
||||
Env: envs,
|
||||
Resources: jenkinsContainer.Resources,
|
||||
VolumeMounts: append(GetJenkinsMasterContainerBaseVolumeMounts(), jenkinsContainer.VolumeMounts...),
|
||||
VolumeMounts: append(GetJenkinsMasterContainerBaseVolumeMounts(jenkins), jenkinsContainer.VolumeMounts...),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -272,7 +274,6 @@ func GetJenkinsMasterPodName(jenkins v1alpha2.Jenkins) string {
|
|||
|
||||
// NewJenkinsMasterPod builds Jenkins Master Kubernetes Pod resource
|
||||
func NewJenkinsMasterPod(objectMeta metav1.ObjectMeta, jenkins *v1alpha2.Jenkins) *corev1.Pod {
|
||||
|
||||
serviceAccountName := objectMeta.Name
|
||||
objectMeta.Annotations = jenkins.Spec.Master.Annotations
|
||||
objectMeta.Name = GetJenkinsMasterPodName(*jenkins)
|
||||
|
|
@ -283,10 +284,11 @@ func NewJenkinsMasterPod(objectMeta metav1.ObjectMeta, jenkins *v1alpha2.Jenkins
|
|||
Spec: corev1.PodSpec{
|
||||
ServiceAccountName: serviceAccountName,
|
||||
RestartPolicy: corev1.RestartPolicyNever,
|
||||
SecurityContext: jenkins.Spec.Master.SecurityContext,
|
||||
NodeSelector: jenkins.Spec.Master.NodeSelector,
|
||||
Containers: newContainers(jenkins),
|
||||
Volumes: append(GetJenkinsMasterPodBaseVolumes(jenkins), jenkins.Spec.Master.Volumes...),
|
||||
SecurityContext: jenkins.Spec.Master.SecurityContext,
|
||||
ImagePullSecrets: jenkins.Spec.Master.ImagePullSecrets,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,16 +7,8 @@ import (
|
|||
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func buildServiceTypeMeta() metav1.TypeMeta {
|
||||
return metav1.TypeMeta{
|
||||
Kind: "Service",
|
||||
APIVersion: "v1",
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateService returns new service with override fields from config
|
||||
func UpdateService(actual corev1.Service, config v1alpha2.Service) corev1.Service {
|
||||
actual.ObjectMeta.Annotations = config.Annotations
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
package resources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
const configureTheme = `
|
||||
import jenkins.*
|
||||
import jenkins.model.*
|
||||
import hudson.*
|
||||
import hudson.model.*
|
||||
import org.jenkinsci.plugins.simpletheme.ThemeElement
|
||||
import org.jenkinsci.plugins.simpletheme.CssTextThemeElement
|
||||
import org.jenkinsci.plugins.simpletheme.CssUrlThemeElement
|
||||
|
||||
Jenkins jenkins = Jenkins.getInstance()
|
||||
|
||||
def decorator = Jenkins.instance.getDescriptorByType(org.codefirst.SimpleThemeDecorator.class)
|
||||
|
||||
List<ThemeElement> configElements = new ArrayList<>();
|
||||
configElements.add(new CssTextThemeElement("DEFAULT"));
|
||||
configElements.add(new CssUrlThemeElement("https://cdn.rawgit.com/afonsof/jenkins-material-theme/gh-pages/dist/material-light-green.css"));
|
||||
decorator.setElements(configElements);
|
||||
decorator.save();
|
||||
|
||||
jenkins.save()
|
||||
`
|
||||
|
||||
// GetUserConfigurationConfigMapNameFromJenkins returns name of Kubernetes config map used to user configuration
|
||||
func GetUserConfigurationConfigMapNameFromJenkins(jenkins *v1alpha2.Jenkins) string {
|
||||
return fmt.Sprintf("%s-user-configuration-%s", constants.OperatorName, jenkins.ObjectMeta.Name)
|
||||
}
|
||||
|
||||
// GetUserConfigurationConfigMapName returns name of Kubernetes config map used to user configuration
|
||||
func GetUserConfigurationConfigMapName(jenkinsCRName string) string {
|
||||
return fmt.Sprintf("%s-user-configuration-%s", constants.OperatorName, jenkinsCRName)
|
||||
}
|
||||
|
||||
// NewUserConfigurationConfigMap builds Kubernetes config map used to user configuration
|
||||
func NewUserConfigurationConfigMap(jenkins *v1alpha2.Jenkins) *corev1.ConfigMap {
|
||||
return &corev1.ConfigMap{
|
||||
TypeMeta: buildConfigMapTypeMeta(),
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: GetUserConfigurationConfigMapNameFromJenkins(jenkins),
|
||||
Namespace: jenkins.ObjectMeta.Namespace,
|
||||
Labels: BuildLabelsForWatchedResources(*jenkins),
|
||||
},
|
||||
Data: map[string]string{
|
||||
"1-configure-theme.groovy": configureTheme,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
package resources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// GetUserConfigurationSecretNameFromJenkins returns name of Kubernetes secret used to store jenkins operator credentials
|
||||
func GetUserConfigurationSecretNameFromJenkins(jenkins *v1alpha2.Jenkins) string {
|
||||
return fmt.Sprintf("%s-user-configuration-%s", constants.OperatorName, jenkins.Name)
|
||||
}
|
||||
|
||||
// GetUserConfigurationSecretName returns name of Kubernetes secret used to store jenkins operator credentials
|
||||
func GetUserConfigurationSecretName(jenkinsCRName string) string {
|
||||
return fmt.Sprintf("%s-user-configuration-%s", constants.OperatorName, jenkinsCRName)
|
||||
}
|
||||
|
||||
// NewUserConfigurationSecret builds the Kubernetes secret resource which is used to store user sensitive data for Jenkins configuration
|
||||
func NewUserConfigurationSecret(jenkins *v1alpha2.Jenkins) *corev1.Secret {
|
||||
return &corev1.Secret{
|
||||
TypeMeta: buildServiceTypeMeta(),
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: GetUserConfigurationSecretNameFromJenkins(jenkins),
|
||||
Namespace: jenkins.ObjectMeta.Namespace,
|
||||
Labels: BuildLabelsForWatchedResources(*jenkins),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -46,6 +46,58 @@ func (r *ReconcileJenkinsBaseConfiguration) Validate(jenkins *v1alpha2.Jenkins)
|
|||
return false, nil
|
||||
}
|
||||
|
||||
if valid, err := r.validateCustomization(r.jenkins.Spec.GroovyScripts.Customization, "spec.groovyScripts"); err != nil {
|
||||
return false, err
|
||||
} else if !valid {
|
||||
return false, nil
|
||||
}
|
||||
if valid, err := r.validateCustomization(r.jenkins.Spec.ConfigurationAsCode.Customization, "spec.configurationAsCode"); err != nil {
|
||||
return false, err
|
||||
} else if !valid {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *ReconcileJenkinsBaseConfiguration) validateImagePullSecrets() (bool, error) {
|
||||
var err error
|
||||
for _, sr := range r.jenkins.Spec.Master.ImagePullSecrets {
|
||||
valid, err := r.validateImagePullSecret(sr.Name)
|
||||
if err != nil || !valid {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
return true, err
|
||||
}
|
||||
|
||||
func (r *ReconcileJenkinsBaseConfiguration) validateImagePullSecret(name string) (bool, error) {
|
||||
secret := &corev1.Secret{}
|
||||
err := r.k8sClient.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: r.jenkins.ObjectMeta.Namespace}, secret)
|
||||
if err != nil && apierrors.IsNotFound(err) {
|
||||
r.logger.V(log.VWarn).Info(fmt.Sprintf("Secret %s not found defined in spec.master.imagePullSecrets", name))
|
||||
return false, nil
|
||||
} else if err != nil && !apierrors.IsNotFound(err) {
|
||||
return false, stackerr.WithStack(err)
|
||||
}
|
||||
|
||||
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.", name))
|
||||
return false, 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.", name))
|
||||
return false, 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.", name))
|
||||
return false, 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.", name))
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
|
|
@ -194,7 +246,7 @@ func (r *ReconcileJenkinsBaseConfiguration) validateContainerVolumeMounts(contai
|
|||
}
|
||||
|
||||
func (r *ReconcileJenkinsBaseConfiguration) validateJenkinsMasterPodEnvs() bool {
|
||||
baseEnvs := resources.GetJenkinsMasterContainerBaseEnvs()
|
||||
baseEnvs := resources.GetJenkinsMasterContainerBaseEnvs(r.jenkins)
|
||||
baseEnvNames := map[string]string{}
|
||||
for _, env := range baseEnvs {
|
||||
baseEnvNames[env.Name] = env.Value
|
||||
|
|
@ -269,3 +321,45 @@ func (r *ReconcileJenkinsBaseConfiguration) verifyBasePlugins(requiredBasePlugin
|
|||
|
||||
return valid
|
||||
}
|
||||
|
||||
func (r *ReconcileJenkinsBaseConfiguration) validateCustomization(customization v1alpha2.Customization, name string) (bool, error) {
|
||||
valid := true
|
||||
if len(customization.Secret.Name) == 0 && len(customization.Configurations) == 0 {
|
||||
return true, nil
|
||||
}
|
||||
if len(customization.Secret.Name) > 0 && len(customization.Configurations) == 0 {
|
||||
valid = false
|
||||
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 {
|
||||
secret := &corev1.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) {
|
||||
valid = false
|
||||
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) {
|
||||
return false, stackerr.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
for index, configMapRef := range customization.Configurations {
|
||||
if len(configMapRef.Name) == 0 {
|
||||
r.logger.V(log.VWarn).Info(fmt.Sprintf("%s.configurations[%d] name is empty", name, index))
|
||||
valid = false
|
||||
continue
|
||||
}
|
||||
|
||||
configMap := &corev1.ConfigMap{}
|
||||
err := r.k8sClient.Get(context.TODO(), types.NamespacedName{Name: configMapRef.Name, Namespace: r.jenkins.ObjectMeta.Namespace}, configMap)
|
||||
if err != nil && apierrors.IsNotFound(err) {
|
||||
valid = false
|
||||
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) {
|
||||
return false, stackerr.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return valid, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/jenkinsci/kubernetes-operator/pkg/log"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/api/core/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
|
@ -131,6 +132,195 @@ func TestValidatePlugins(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestReconcileJenkinsBaseConfiguration_validateImagePullSecrets(t *testing.T) {
|
||||
t.Run("happy", func(t *testing.T) {
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-ref",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"docker-server": []byte("test_server"),
|
||||
"docker-username": []byte("test_user"),
|
||||
"docker-password": []byte("test_password"),
|
||||
"docker-email": []byte("test_email"),
|
||||
},
|
||||
}
|
||||
|
||||
jenkins := v1alpha2.Jenkins{
|
||||
Spec: v1alpha2.JenkinsSpec{
|
||||
Master: v1alpha2.JenkinsMaster{
|
||||
ImagePullSecrets: []corev1.LocalObjectReference{
|
||||
{Name: secret.ObjectMeta.Name},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fakeClient := fake.NewFakeClient()
|
||||
err := fakeClient.Create(context.TODO(), secret)
|
||||
assert.NoError(t, err)
|
||||
|
||||
baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false),
|
||||
&jenkins, false, false, nil, nil)
|
||||
|
||||
got, err := baseReconcileLoop.validateImagePullSecrets()
|
||||
assert.Equal(t, got, true)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("no secret", func(t *testing.T) {
|
||||
jenkins := v1alpha2.Jenkins{
|
||||
Spec: v1alpha2.JenkinsSpec{
|
||||
Master: v1alpha2.JenkinsMaster{
|
||||
ImagePullSecrets: []corev1.LocalObjectReference{
|
||||
{Name: "test-ref"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fakeClient := fake.NewFakeClient()
|
||||
|
||||
baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false),
|
||||
&jenkins, false, false, nil, nil)
|
||||
|
||||
got, _ := baseReconcileLoop.validateImagePullSecrets()
|
||||
assert.Equal(t, got, false)
|
||||
})
|
||||
|
||||
t.Run("no docker email", func(t *testing.T) {
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-ref",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"docker-server": []byte("test_server"),
|
||||
"docker-username": []byte("test_user"),
|
||||
"docker-password": []byte("test_password"),
|
||||
},
|
||||
}
|
||||
|
||||
jenkins := v1alpha2.Jenkins{
|
||||
Spec: v1alpha2.JenkinsSpec{
|
||||
Master: v1alpha2.JenkinsMaster{
|
||||
ImagePullSecrets: []corev1.LocalObjectReference{
|
||||
{Name: secret.ObjectMeta.Name},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fakeClient := fake.NewFakeClient()
|
||||
err := fakeClient.Create(context.TODO(), secret)
|
||||
assert.NoError(t, err)
|
||||
|
||||
baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false),
|
||||
&jenkins, false, false, nil, nil)
|
||||
|
||||
got, _ := baseReconcileLoop.validateImagePullSecrets()
|
||||
assert.Equal(t, got, false)
|
||||
})
|
||||
|
||||
t.Run("no docker password", func(t *testing.T) {
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-ref",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"docker-server": []byte("test_server"),
|
||||
"docker-username": []byte("test_user"),
|
||||
"docker-email": []byte("test_email"),
|
||||
},
|
||||
}
|
||||
|
||||
jenkins := v1alpha2.Jenkins{
|
||||
Spec: v1alpha2.JenkinsSpec{
|
||||
Master: v1alpha2.JenkinsMaster{
|
||||
ImagePullSecrets: []corev1.LocalObjectReference{
|
||||
{Name: secret.ObjectMeta.Name},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fakeClient := fake.NewFakeClient()
|
||||
err := fakeClient.Create(context.TODO(), secret)
|
||||
assert.NoError(t, err)
|
||||
|
||||
baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false),
|
||||
&jenkins, false, false, nil, nil)
|
||||
|
||||
got, _ := baseReconcileLoop.validateImagePullSecrets()
|
||||
assert.Equal(t, got, false)
|
||||
})
|
||||
|
||||
t.Run("no docker username", func(t *testing.T) {
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-ref",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"docker-server": []byte("test_server"),
|
||||
"docker-password": []byte("test_password"),
|
||||
"docker-email": []byte("test_email"),
|
||||
},
|
||||
}
|
||||
|
||||
jenkins := v1alpha2.Jenkins{
|
||||
Spec: v1alpha2.JenkinsSpec{
|
||||
Master: v1alpha2.JenkinsMaster{
|
||||
ImagePullSecrets: []corev1.LocalObjectReference{
|
||||
{Name: secret.ObjectMeta.Name},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fakeClient := fake.NewFakeClient()
|
||||
err := fakeClient.Create(context.TODO(), secret)
|
||||
assert.NoError(t, err)
|
||||
|
||||
baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false),
|
||||
&jenkins, false, false, nil, nil)
|
||||
|
||||
got, _ := baseReconcileLoop.validateImagePullSecrets()
|
||||
assert.Equal(t, got, false)
|
||||
})
|
||||
|
||||
t.Run("no docker server", func(t *testing.T) {
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-ref",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"docker-username": []byte("test_user"),
|
||||
"docker-password": []byte("test_password"),
|
||||
"docker-email": []byte("test_email"),
|
||||
},
|
||||
}
|
||||
|
||||
jenkins := v1alpha2.Jenkins{
|
||||
Spec: v1alpha2.JenkinsSpec{
|
||||
Master: v1alpha2.JenkinsMaster{
|
||||
ImagePullSecrets: []corev1.LocalObjectReference{
|
||||
{Name: secret.ObjectMeta.Name},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fakeClient := fake.NewFakeClient()
|
||||
err := fakeClient.Create(context.TODO(), secret)
|
||||
assert.NoError(t, err)
|
||||
|
||||
baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false),
|
||||
&jenkins, false, false, nil, nil)
|
||||
|
||||
got, _ := baseReconcileLoop.validateImagePullSecrets()
|
||||
assert.Equal(t, got, false)
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateJenkinsMasterPodEnvs(t *testing.T) {
|
||||
t.Run("happy", func(t *testing.T) {
|
||||
jenkins := v1alpha2.Jenkins{
|
||||
|
|
@ -446,3 +636,122 @@ func TestValidateSecretVolume(t *testing.T) {
|
|||
assert.False(t, got)
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateCustomization(t *testing.T) {
|
||||
namespace := "default"
|
||||
secretName := "secretName"
|
||||
configMapName := "configmap-name"
|
||||
jenkins := &v1alpha2.Jenkins{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: namespace,
|
||||
},
|
||||
}
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
customization := v1alpha2.Customization{}
|
||||
fakeClient := fake.NewFakeClient()
|
||||
baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false),
|
||||
jenkins, false, false, nil, nil)
|
||||
|
||||
got, err := baseReconcileLoop.validateCustomization(customization, "spec.groovyScripts")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, got)
|
||||
})
|
||||
t.Run("secret set but configurations is empty", func(t *testing.T) {
|
||||
customization := v1alpha2.Customization{
|
||||
Secret: v1alpha2.SecretRef{Name: secretName},
|
||||
Configurations: []v1alpha2.ConfigMapRef{},
|
||||
}
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: secretName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
}
|
||||
fakeClient := fake.NewFakeClient()
|
||||
baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false),
|
||||
jenkins, false, false, nil, nil)
|
||||
err := fakeClient.Create(context.TODO(), secret)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := baseReconcileLoop.validateCustomization(customization, "spec.groovyScripts")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, got)
|
||||
})
|
||||
t.Run("secret and configmap exists", func(t *testing.T) {
|
||||
customization := v1alpha2.Customization{
|
||||
Secret: v1alpha2.SecretRef{Name: secretName},
|
||||
Configurations: []v1alpha2.ConfigMapRef{{Name: configMapName}},
|
||||
}
|
||||
configMap := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: configMapName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
}
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: secretName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
}
|
||||
fakeClient := fake.NewFakeClient()
|
||||
baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false),
|
||||
jenkins, false, false, nil, nil)
|
||||
err := fakeClient.Create(context.TODO(), secret)
|
||||
require.NoError(t, err)
|
||||
err = fakeClient.Create(context.TODO(), configMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := baseReconcileLoop.validateCustomization(customization, "spec.groovyScripts")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, got)
|
||||
})
|
||||
t.Run("secret not exists and configmap exists", func(t *testing.T) {
|
||||
configMapName := "configmap-name"
|
||||
customization := v1alpha2.Customization{
|
||||
Secret: v1alpha2.SecretRef{Name: secretName},
|
||||
Configurations: []v1alpha2.ConfigMapRef{{Name: configMapName}},
|
||||
}
|
||||
configMap := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: configMapName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
}
|
||||
fakeClient := fake.NewFakeClient()
|
||||
baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false),
|
||||
jenkins, false, false, nil, nil)
|
||||
err := fakeClient.Create(context.TODO(), configMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := baseReconcileLoop.validateCustomization(customization, "spec.groovyScripts")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, got)
|
||||
})
|
||||
t.Run("secret exists and configmap not exists", func(t *testing.T) {
|
||||
customization := v1alpha2.Customization{
|
||||
Secret: v1alpha2.SecretRef{Name: secretName},
|
||||
Configurations: []v1alpha2.ConfigMapRef{{Name: configMapName}},
|
||||
}
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: secretName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
}
|
||||
fakeClient := fake.NewFakeClient()
|
||||
baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false),
|
||||
jenkins, false, false, nil, nil)
|
||||
err := fakeClient.Create(context.TODO(), secret)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := baseReconcileLoop.validateCustomization(customization, "spec.groovyScripts")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, got)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,216 +1,50 @@
|
|||
package casc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
|
||||
jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client"
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources"
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/jobs"
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/groovy"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/pkg/errors"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
k8s "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
const (
|
||||
userConfigurationHashParameterName = "userConfigurationHash"
|
||||
userConfigurationSecretHashParameterName = "userConfigurationSecretHash"
|
||||
)
|
||||
|
||||
// ConfigurationAsCode defines API which configures Jenkins with help Configuration as a code plugin
|
||||
type ConfigurationAsCode struct {
|
||||
jenkinsClient jenkinsclient.Jenkins
|
||||
k8sClient k8s.Client
|
||||
logger logr.Logger
|
||||
jobName string
|
||||
groovyClient *groovy.Groovy
|
||||
}
|
||||
|
||||
// New creates new instance of ConfigurationAsCode
|
||||
func New(jenkinsClient jenkinsclient.Jenkins, k8sClient k8s.Client, logger logr.Logger, jobName string) *ConfigurationAsCode {
|
||||
func New(jenkinsClient jenkinsclient.Jenkins, k8sClient k8s.Client, logger logr.Logger, jenkins *v1alpha2.Jenkins) *ConfigurationAsCode {
|
||||
return &ConfigurationAsCode{
|
||||
jenkinsClient: jenkinsClient,
|
||||
k8sClient: k8sClient,
|
||||
logger: logger,
|
||||
jobName: jobName,
|
||||
groovyClient: groovy.New(jenkinsClient, k8sClient, logger, jenkins, "user-casc", jenkins.Spec.ConfigurationAsCode.Customization),
|
||||
}
|
||||
}
|
||||
|
||||
// ConfigureJob configures jenkins job which configures Jenkins with help Configuration as a code plugin
|
||||
func (g *ConfigurationAsCode) ConfigureJob() error {
|
||||
_, created, err := g.jenkinsClient.CreateOrUpdateJob(configurationJobXMLFmt, g.jobName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if created {
|
||||
g.logger.Info(fmt.Sprintf("'%s' job has been created", g.jobName))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure configures Jenkins with help Configuration as a code plugin
|
||||
func (g *ConfigurationAsCode) Ensure(jenkins *v1alpha2.Jenkins) (bool, error) {
|
||||
jobsClient := jobs.New(g.jenkinsClient, g.k8sClient, g.logger)
|
||||
|
||||
configuration := &corev1.ConfigMap{}
|
||||
ConfigMapNamespaceName := types.NamespacedName{Namespace: jenkins.Namespace, Name: resources.GetUserConfigurationConfigMapNameFromJenkins(jenkins)}
|
||||
err := g.k8sClient.Get(context.TODO(), ConfigMapNamespaceName, configuration)
|
||||
if err != nil {
|
||||
return false, errors.WithStack(err)
|
||||
func (c *ConfigurationAsCode) Ensure(jenkins *v1alpha2.Jenkins) (requeue bool, err error) {
|
||||
requeue, err = c.groovyClient.WaitForSecretSynchronization(resources.ConfigurationAsCodeSecretVolumePath)
|
||||
if err != nil || requeue {
|
||||
return requeue, err
|
||||
}
|
||||
|
||||
secret := &corev1.Secret{}
|
||||
secretNamespaceName := types.NamespacedName{Namespace: jenkins.Namespace, Name: resources.GetUserConfigurationSecretNameFromJenkins(jenkins)}
|
||||
err = g.k8sClient.Get(context.TODO(), secretNamespaceName, secret)
|
||||
if err != nil {
|
||||
return false, errors.WithStack(err)
|
||||
}
|
||||
|
||||
userConfigurationSecretHash := g.calculateUserConfigurationSecretHash(secret)
|
||||
userConfigurationHash := g.calculateUserConfigurationHash(configuration)
|
||||
done, err := jobsClient.EnsureBuildJob(
|
||||
g.jobName,
|
||||
userConfigurationSecretHash+userConfigurationHash,
|
||||
map[string]string{
|
||||
userConfigurationHashParameterName: userConfigurationHash,
|
||||
userConfigurationSecretHashParameterName: userConfigurationSecretHash,
|
||||
},
|
||||
jenkins,
|
||||
true)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return done, nil
|
||||
return c.groovyClient.Ensure(func(name string) bool {
|
||||
return strings.HasSuffix(name, ".yaml") || strings.HasSuffix(name, ".yml")
|
||||
}, func(groovyScript string) string {
|
||||
return fmt.Sprintf(applyConfigurationAsCodeGroovyScriptFmt, groovyScript)
|
||||
})
|
||||
}
|
||||
|
||||
func (g *ConfigurationAsCode) calculateUserConfigurationSecretHash(userConfigurationSecret *corev1.Secret) string {
|
||||
hash := sha256.New()
|
||||
const applyConfigurationAsCodeGroovyScriptFmt = `
|
||||
def config = '''
|
||||
%s
|
||||
'''
|
||||
def stream = new ByteArrayInputStream(config.getBytes('UTF-8'))
|
||||
|
||||
var keys []string
|
||||
for key := range userConfigurationSecret.Data {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
hash.Write([]byte(key))
|
||||
hash.Write([]byte(userConfigurationSecret.Data[key]))
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
||||
func (g *ConfigurationAsCode) calculateUserConfigurationHash(userConfiguration *corev1.ConfigMap) string {
|
||||
hash := sha256.New()
|
||||
|
||||
var keys []string
|
||||
for key := range userConfiguration.Data {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
if strings.HasSuffix(key, ".yaml") {
|
||||
hash.Write([]byte(key))
|
||||
hash.Write([]byte(userConfiguration.Data[key]))
|
||||
}
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
||||
const configurationJobXMLFmt = `<?xml version='1.1' encoding='UTF-8'?>
|
||||
<flow-definition plugin="workflow-job@2.31">
|
||||
<actions/>
|
||||
<description></description>
|
||||
<keepDependencies>false</keepDependencies>
|
||||
<properties>
|
||||
<org.jenkinsci.plugins.workflow.job.properties.DisableConcurrentBuildsJobProperty/>
|
||||
<hudson.model.ParametersDefinitionProperty>
|
||||
<parameterDefinitions>
|
||||
<hudson.model.StringParameterDefinition>
|
||||
<name>` + userConfigurationSecretHashParameterName + `</name>
|
||||
<description/>
|
||||
<defaultValue/>
|
||||
<trim>false</trim>
|
||||
</hudson.model.StringParameterDefinition>
|
||||
<hudson.model.StringParameterDefinition>
|
||||
<name>` + userConfigurationHashParameterName + `</name>
|
||||
<description/>
|
||||
<defaultValue/>
|
||||
<trim>false</trim>
|
||||
</hudson.model.StringParameterDefinition>
|
||||
</parameterDefinitions>
|
||||
</hudson.model.ParametersDefinitionProperty>
|
||||
</properties>
|
||||
<definition class="org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition" plugin="workflow-cps@2.61.1">
|
||||
<script>import io.jenkins.plugins.casc.yaml.YamlSource;
|
||||
|
||||
def secretsPath = '` + resources.UserConfigurationSecretVolumePath + `'
|
||||
def configsPath = '` + resources.JenkinsUserConfigurationVolumePath + `'
|
||||
def userConfigurationSecretExpectedHash = params.` + userConfigurationSecretHashParameterName + `
|
||||
def userConfigurationExpectedHash = params.` + userConfigurationHashParameterName + `
|
||||
|
||||
node('master') {
|
||||
def secretsText = sh(script: "ls ${secretsPath} | sort", returnStdout: true).trim()
|
||||
def secrets = []
|
||||
secrets.addAll(secretsText.tokenize('\n'))
|
||||
|
||||
def configsText = sh(script: "ls ${configsPath} | grep .yaml | sort", returnStdout: true).trim()
|
||||
def configs = []
|
||||
configs.addAll(configsText.tokenize('\n'))
|
||||
|
||||
stage('Synchronizing files') {
|
||||
println "Synchronizing Kubernetes ConfigMaps and Secrets to the Jenkins master pod."
|
||||
println "This step may fail and will be retried in the next job build if necessary."
|
||||
|
||||
synchronizeFiles(secretsPath, (String[])secrets, userConfigurationSecretExpectedHash)
|
||||
synchronizeFiles(configsPath, (String[])configs, userConfigurationExpectedHash)
|
||||
}
|
||||
|
||||
for(config in configs) {
|
||||
stage(config) {
|
||||
def path = java.nio.file.Paths.get("${configsPath}/${config}")
|
||||
def source = new YamlSource(path, YamlSource.READ_FROM_PATH)
|
||||
io.jenkins.plugins.casc.ConfigurationAsCode.get().configureWith(source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def synchronizeFiles(String path, String[] files, String hash) {
|
||||
def complete = false
|
||||
for(int i = 1; i <= 10; i++) {
|
||||
def actualHash = calculateHash(files, path)
|
||||
println "Expected hash '${hash}', actual hash '${actualHash}', path '${path}', will retry"
|
||||
if(hash == actualHash) {
|
||||
complete = true
|
||||
break
|
||||
}
|
||||
sleep 2
|
||||
}
|
||||
if(!complete) {
|
||||
error("Timeout while synchronizing files")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@NonCPS
|
||||
def calculateHash(String[] configs, String configsPath) {
|
||||
def hash = java.security.MessageDigest.getInstance("SHA-256")
|
||||
for(config in configs) {
|
||||
hash.update(config.getBytes())
|
||||
def fileLocation = java.nio.file.Paths.get("${configsPath}/${config}")
|
||||
def fileData = java.nio.file.Files.readAllBytes(fileLocation)
|
||||
hash.update(fileData)
|
||||
}
|
||||
return Base64.getEncoder().encodeToString(hash.digest())
|
||||
}</script>
|
||||
<sandbox>false</sandbox>
|
||||
</definition>
|
||||
<triggers/>
|
||||
<disabled>false</disabled>
|
||||
</flow-definition>
|
||||
def source = new io.jenkins.plugins.casc.yaml.YamlSource(stream, io.jenkins.plugins.casc.yaml.YamlSource.READ_FROM_INPUTSTREAM)
|
||||
io.jenkins.plugins.casc.ConfigurationAsCode.get().configureWith(source)
|
||||
`
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
|
||||
|
|
@ -10,14 +10,11 @@ import (
|
|||
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources"
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/user/casc"
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/user/seedjobs"
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants"
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/groovy"
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/jobs"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/pkg/errors"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
k8s "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
|
@ -104,37 +101,32 @@ func (r *ReconcileUserConfiguration) ensureSeedJobs() (reconcile.Result, error)
|
|||
}
|
||||
|
||||
func (r *ReconcileUserConfiguration) ensureUserConfiguration(jenkinsClient jenkinsclient.Jenkins) (reconcile.Result, error) {
|
||||
configuration := &corev1.ConfigMap{}
|
||||
namespaceName := types.NamespacedName{Namespace: r.jenkins.Namespace, Name: resources.GetUserConfigurationConfigMapNameFromJenkins(r.jenkins)}
|
||||
err := r.k8sClient.Get(context.TODO(), namespaceName, configuration)
|
||||
groovyClient := groovy.New(jenkinsClient, r.k8sClient, r.logger, r.jenkins, "user-groovy", r.jenkins.Spec.GroovyScripts.Customization)
|
||||
|
||||
requeue, err := groovyClient.WaitForSecretSynchronization(resources.GroovyScriptsSecretVolumePath)
|
||||
if err != nil {
|
||||
return reconcile.Result{}, errors.WithStack(err)
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
if requeue {
|
||||
return reconcile.Result{Requeue: true}, nil
|
||||
}
|
||||
requeue, err = groovyClient.Ensure(func(name string) bool {
|
||||
return strings.HasSuffix(name, ".groovy")
|
||||
}, groovy.AddSecretsLoaderToGroovyScript(resources.GroovyScriptsSecretVolumePath))
|
||||
if err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
if requeue {
|
||||
return reconcile.Result{Requeue: true}, nil
|
||||
}
|
||||
|
||||
groovyClient := groovy.New(jenkinsClient, r.k8sClient, r.logger, constants.UserConfigurationJobName, resources.JenkinsUserConfigurationVolumePath)
|
||||
err = groovyClient.ConfigureJob()
|
||||
configurationAsCodeClient := casc.New(jenkinsClient, r.k8sClient, r.logger, r.jenkins)
|
||||
requeue, err = configurationAsCodeClient.Ensure(r.jenkins)
|
||||
if err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
done, err := groovyClient.Ensure(configuration.Data, r.jenkins)
|
||||
if err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
if !done {
|
||||
return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 10}, nil
|
||||
}
|
||||
|
||||
configurationAsCodeClient := casc.New(jenkinsClient, r.k8sClient, r.logger, constants.UserConfigurationCASCJobName)
|
||||
err = configurationAsCodeClient.ConfigureJob()
|
||||
if err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
done, err = configurationAsCodeClient.Ensure(r.jenkins)
|
||||
if err != nil {
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
if !done {
|
||||
return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 10}, nil
|
||||
if requeue {
|
||||
return reconcile.Result{Requeue: true}, nil
|
||||
}
|
||||
|
||||
return reconcile.Result{}, nil
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package groovy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
|
@ -9,144 +10,238 @@ import (
|
|||
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
|
||||
jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client"
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/jobs"
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/log"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"github.com/pkg/errors"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
k8s "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
const (
|
||||
jobHashParameterName = "hash"
|
||||
)
|
||||
|
||||
// Groovy defines API for groovy scripts execution via jenkins job
|
||||
// Groovy defines API for groovy secrets execution via jenkins job
|
||||
type Groovy struct {
|
||||
jenkinsClient jenkinsclient.Jenkins
|
||||
k8sClient k8s.Client
|
||||
logger logr.Logger
|
||||
jobName string
|
||||
scriptsPath string
|
||||
k8sClient k8s.Client
|
||||
logger logr.Logger
|
||||
jenkins *v1alpha2.Jenkins
|
||||
jenkinsClient jenkinsclient.Jenkins
|
||||
configurationType string
|
||||
customization v1alpha2.Customization
|
||||
}
|
||||
|
||||
// New creates new instance of Groovy
|
||||
func New(jenkinsClient jenkinsclient.Jenkins, k8sClient k8s.Client, logger logr.Logger, jobName, scriptsPath string) *Groovy {
|
||||
func New(jenkinsClient jenkinsclient.Jenkins, k8sClient k8s.Client, logger logr.Logger, jenkins *v1alpha2.Jenkins, configurationType string, customization v1alpha2.Customization) *Groovy {
|
||||
return &Groovy{
|
||||
jenkinsClient: jenkinsClient,
|
||||
k8sClient: k8sClient,
|
||||
logger: logger,
|
||||
jobName: jobName,
|
||||
scriptsPath: scriptsPath,
|
||||
jenkinsClient: jenkinsClient,
|
||||
k8sClient: k8sClient,
|
||||
logger: logger,
|
||||
jenkins: jenkins,
|
||||
configurationType: configurationType,
|
||||
customization: customization,
|
||||
}
|
||||
}
|
||||
|
||||
// ConfigureJob configures jenkins job for executing groovy scripts
|
||||
func (g *Groovy) ConfigureJob() error {
|
||||
_, created, err := g.jenkinsClient.CreateOrUpdateJob(fmt.Sprintf(configurationJobXMLFmt, g.scriptsPath), g.jobName)
|
||||
// EnsureSingle runs single groovy script
|
||||
func (g *Groovy) EnsureSingle(source, name, hash, groovyScript string) (requeue bool, err error) {
|
||||
if g.isGroovyScriptAlreadyApplied(source, name, hash) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
logs, err := g.jenkinsClient.ExecuteScript(groovyScript)
|
||||
if err != nil {
|
||||
return err
|
||||
if _, ok := err.(*jenkinsclient.GroovyScriptExecutionFailed); ok {
|
||||
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
|
||||
}
|
||||
if created {
|
||||
g.logger.Info(fmt.Sprintf("'%s' job has been created", g.jobName))
|
||||
|
||||
var appliedGroovyScripts []v1alpha2.AppliedGroovyScript
|
||||
|
||||
for _, ags := range g.jenkins.Status.AppliedGroovyScripts {
|
||||
if g.configurationType == ags.ConfigurationType && ags.Source == source && ags.Name == name {
|
||||
continue
|
||||
}
|
||||
|
||||
appliedGroovyScripts = append(appliedGroovyScripts, ags)
|
||||
}
|
||||
return nil
|
||||
appliedGroovyScripts = append(appliedGroovyScripts, v1alpha2.AppliedGroovyScript{
|
||||
ConfigurationType: g.configurationType,
|
||||
Source: source,
|
||||
Name: name,
|
||||
Hash: hash,
|
||||
})
|
||||
|
||||
g.jenkins.Status.AppliedGroovyScripts = appliedGroovyScripts
|
||||
|
||||
return true, g.k8sClient.Update(context.TODO(), g.jenkins)
|
||||
}
|
||||
|
||||
// Ensure executes groovy script and verifies jenkins job status according to reconciliation loop lifecycle
|
||||
func (g *Groovy) Ensure(secretOrConfigMapData map[string]string, jenkins *v1alpha2.Jenkins) (bool, error) {
|
||||
jobsClient := jobs.New(g.jenkinsClient, g.k8sClient, g.logger)
|
||||
// WaitForSecretSynchronization runs groovy script which waits to synchronize secrets in pod by k8s
|
||||
func (g *Groovy) WaitForSecretSynchronization(secretsPath string) (requeue bool, err error) {
|
||||
if len(g.customization.Secret.Name) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
hash := g.calculateHash(secretOrConfigMapData)
|
||||
done, err := jobsClient.EnsureBuildJob(g.jobName, hash, map[string]string{jobHashParameterName: hash}, jenkins, true)
|
||||
secret := &corev1.Secret{}
|
||||
err = g.k8sClient.Get(context.TODO(), types.NamespacedName{Name: g.customization.Secret.Name, Namespace: g.jenkins.ObjectMeta.Namespace}, secret)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return true, errors.WithStack(err)
|
||||
}
|
||||
return done, nil
|
||||
|
||||
toCalculate := map[string]string{}
|
||||
for secretKey, secretValue := range secret.Data {
|
||||
toCalculate[secretKey] = string(secretValue)
|
||||
}
|
||||
hash := g.calculateHash(toCalculate)
|
||||
|
||||
name := "synchronizing-secret.groovy"
|
||||
if g.isGroovyScriptAlreadyApplied(g.customization.Secret.Name, name, hash) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
g.logger.Info(fmt.Sprintf("%s Secret '%s' running synchronization", g.configurationType, secret.Name))
|
||||
return g.EnsureSingle(g.customization.Secret.Name, name, hash, fmt.Sprintf(synchronizeSecretsGroovyScriptFmt, secretsPath, hash))
|
||||
}
|
||||
|
||||
func (g *Groovy) calculateHash(secretOrConfigMapData map[string]string) string {
|
||||
// Ensure runs all groovy scripts configured in customization structure
|
||||
func (g *Groovy) Ensure(filter func(name string) bool, updateGroovyScript func(groovyScript string) string) (requeue bool, err error) {
|
||||
secret := &corev1.Secret{}
|
||||
if len(g.customization.Secret.Name) > 0 {
|
||||
err := g.k8sClient.Get(context.TODO(), types.NamespacedName{Name: g.customization.Secret.Name, Namespace: g.jenkins.ObjectMeta.Namespace}, secret)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
}
|
||||
|
||||
for _, configMapRef := range g.customization.Configurations {
|
||||
configMap := &corev1.ConfigMap{}
|
||||
err := g.k8sClient.Get(context.TODO(), types.NamespacedName{Name: configMapRef.Name, Namespace: g.jenkins.ObjectMeta.Namespace}, configMap)
|
||||
if err != nil {
|
||||
return true, errors.WithStack(err)
|
||||
}
|
||||
|
||||
var names []string
|
||||
for name := range configMap.Data {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
for _, name := range names {
|
||||
groovyScript := updateGroovyScript(configMap.Data[name])
|
||||
if !filter(name) {
|
||||
g.logger.V(log.VDebug).Info(fmt.Sprintf("Skipping %s ConfigMap '%s' name '%s'", g.configurationType, configMap.Name, name))
|
||||
continue
|
||||
}
|
||||
|
||||
hash := g.calculateCustomizationHash(*secret, name, groovyScript)
|
||||
if g.isGroovyScriptAlreadyApplied(configMap.Name, name, hash) {
|
||||
continue
|
||||
}
|
||||
|
||||
g.logger.Info(fmt.Sprintf("%s ConfigMap '%s' name '%s' running groovy script", g.configurationType, configMap.Name, name))
|
||||
requeue, err := g.EnsureSingle(configMap.Name, name, hash, groovyScript)
|
||||
if err != nil || requeue {
|
||||
return requeue, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (g *Groovy) calculateCustomizationHash(secret corev1.Secret, key, groovyScript string) string {
|
||||
toCalculate := map[string]string{}
|
||||
for secretKey, secretValue := range secret.Data {
|
||||
toCalculate[secretKey] = string(secretValue)
|
||||
}
|
||||
toCalculate[key] = groovyScript
|
||||
return g.calculateHash(toCalculate)
|
||||
}
|
||||
|
||||
func (g *Groovy) isGroovyScriptAlreadyApplied(source, name, hash string) bool {
|
||||
for _, appliedGroovyScript := range g.jenkins.Status.AppliedGroovyScripts {
|
||||
if appliedGroovyScript.ConfigurationType == g.configurationType && appliedGroovyScript.Hash == hash &&
|
||||
appliedGroovyScript.Name == name && appliedGroovyScript.Source == source {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (g *Groovy) calculateHash(data map[string]string) string {
|
||||
hash := sha256.New()
|
||||
|
||||
var keys []string
|
||||
for key := range secretOrConfigMapData {
|
||||
for key := range data {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
if strings.HasSuffix(key, ".groovy") {
|
||||
hash.Write([]byte(key))
|
||||
hash.Write([]byte(secretOrConfigMapData[key]))
|
||||
}
|
||||
hash.Write([]byte(key))
|
||||
hash.Write([]byte(data[key]))
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(hash.Sum(nil))
|
||||
}
|
||||
|
||||
const configurationJobXMLFmt = `<?xml version='1.1' encoding='UTF-8'?>
|
||||
<flow-definition plugin="workflow-job@2.31">
|
||||
<actions/>
|
||||
<description></description>
|
||||
<keepDependencies>false</keepDependencies>
|
||||
<properties>
|
||||
<org.jenkinsci.plugins.workflow.job.properties.DisableConcurrentBuildsJobProperty/>
|
||||
<hudson.model.ParametersDefinitionProperty>
|
||||
<parameterDefinitions>
|
||||
<hudson.model.StringParameterDefinition>
|
||||
<name>` + jobHashParameterName + `</name>
|
||||
<description></description>
|
||||
<defaultValue></defaultValue>
|
||||
<trim>false</trim>
|
||||
</hudson.model.StringParameterDefinition>
|
||||
</parameterDefinitions>
|
||||
</hudson.model.ParametersDefinitionProperty>
|
||||
</properties>
|
||||
<definition class="org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition" plugin="workflow-cps@2.61">
|
||||
<script>def scriptsPath = '%s'
|
||||
def expectedHash = params.hash
|
||||
// AddSecretsLoaderToGroovyScript modify groovy scripts to load Kubernetes secrets into groovy map
|
||||
func AddSecretsLoaderToGroovyScript(secretsPath string) func(groovyScript string) string {
|
||||
return func(groovyScript string) string {
|
||||
if !strings.HasPrefix(groovyScript, importPrefix) {
|
||||
return fmt.Sprintf(secretsLoaderGroovyScriptFmt, secretsPath) + groovyScript
|
||||
}
|
||||
|
||||
node('master') {
|
||||
def scriptsText = sh(script: "ls ${scriptsPath} | grep .groovy | sort", returnStdout: true).trim()
|
||||
def scripts = []
|
||||
scripts.addAll(scriptsText.tokenize('\n'))
|
||||
|
||||
stage('Synchronizing files') {
|
||||
println "Synchronizing Kubernetes ConfigMaps to the Jenkins master pod."
|
||||
println "This step may fail and will be retried in the next job build if necessary."
|
||||
lines := strings.Split(groovyScript, "\n")
|
||||
importIndex := -1
|
||||
for i, line := range lines {
|
||||
if !strings.HasPrefix(line, importPrefix) {
|
||||
importIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
def complete = false
|
||||
for(int i = 1; i <= 10; i++) {
|
||||
def actualHash = calculateHash((String[])scripts, scriptsPath)
|
||||
println "Expected hash '${expectedHash}', actual hash '${actualHash}', will retry"
|
||||
if(expectedHash == actualHash) {
|
||||
complete = true
|
||||
break
|
||||
}
|
||||
sleep 2
|
||||
}
|
||||
if(!complete) {
|
||||
error("Timeout while synchronizing files")
|
||||
}
|
||||
}
|
||||
|
||||
for(script in scripts) {
|
||||
stage(script) {
|
||||
load "${scriptsPath}/${script}"
|
||||
}
|
||||
}
|
||||
return strings.Join(lines[:importIndex], "\n") + "\n\n" + fmt.Sprintf(secretsLoaderGroovyScriptFmt, secretsPath) + "\n\n" + strings.Join(lines[importIndex:], "\n")
|
||||
}
|
||||
}
|
||||
|
||||
@NonCPS
|
||||
def calculateHash(String[] scripts, String scriptsPath) {
|
||||
def hash = java.security.MessageDigest.getInstance("SHA-256")
|
||||
for(script in scripts) {
|
||||
hash.update(script.getBytes())
|
||||
def fileLocation = java.nio.file.Paths.get("${scriptsPath}/${script}")
|
||||
const importPrefix = "import "
|
||||
|
||||
const secretsLoaderGroovyScriptFmt = `def secretsPath = '%s'
|
||||
def secrets = [:]
|
||||
"ls ${secretsPath}".execute().text.eachLine {secrets[it] = new File("${secretsPath}/${it}").text}`
|
||||
|
||||
const synchronizeSecretsGroovyScriptFmt = `
|
||||
def secretsPath = '%s'
|
||||
def expectedHash = '%s'
|
||||
|
||||
println "Synchronizing Kubernetes Secret to the Jenkins master pod, timeout 60 seconds."
|
||||
|
||||
def complete = false
|
||||
for(int i = 1; i <= 30; i++) {
|
||||
def fileList = "ls ${secretsPath}".execute()
|
||||
def secrets = []
|
||||
fileList .text.eachLine {secrets.add(it)}
|
||||
println "Mounted secrets: ${secrets}"
|
||||
def actualHash = calculateHash((String[])secrets, secretsPath)
|
||||
println "Expected hash '${expectedHash}', actual hash '${actualHash}', will retry"
|
||||
if(expectedHash == actualHash) {
|
||||
complete = true
|
||||
break
|
||||
}
|
||||
sleep 2000
|
||||
}
|
||||
if(!complete) {
|
||||
throw new Exception("Timeout while synchronizing files")
|
||||
}
|
||||
|
||||
def calculateHash(String[] secrets, String secretsPath) {
|
||||
def hash = java.security.MessageDigest.getInstance("SHA-256")
|
||||
for(secret in secrets) {
|
||||
hash.update(secret.getBytes())
|
||||
def fileLocation = java.nio.file.Paths.get("${secretsPath}/${secret}")
|
||||
def fileData = java.nio.file.Files.readAllBytes(fileLocation)
|
||||
hash.update(fileData)
|
||||
}
|
||||
return Base64.getEncoder().encodeToString(hash.digest())
|
||||
}</script>
|
||||
<sandbox>false</sandbox>
|
||||
</definition>
|
||||
<triggers/>
|
||||
<disabled>false</disabled>
|
||||
</flow-definition>
|
||||
}
|
||||
`
|
||||
|
|
|
|||
|
|
@ -0,0 +1,707 @@
|
|||
package groovy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
|
||||
jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/log"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
)
|
||||
|
||||
func TestGroovy_EnsureSingle(t *testing.T) {
|
||||
log.SetupLogger(true)
|
||||
configurationType := "test-conf-type"
|
||||
emptyCustomization := v1alpha2.Customization{}
|
||||
hash := "hash"
|
||||
groovyScript := "groovy-script"
|
||||
groovyScriptName := "groovy-script-name"
|
||||
source := "source"
|
||||
ctx := context.TODO()
|
||||
jenkinsName := "jenkins"
|
||||
namespace := "default"
|
||||
|
||||
t.Run("execute script and save status", func(t *testing.T) {
|
||||
// given
|
||||
jenkins := &v1alpha2.Jenkins{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: jenkinsName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
}
|
||||
err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme)
|
||||
require.NoError(t, err)
|
||||
fakeClient := fake.NewFakeClient()
|
||||
err = fakeClient.Create(ctx, jenkins)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
jenkinsClient := jenkinsclient.NewMockJenkins(ctrl)
|
||||
jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil)
|
||||
|
||||
groovyClient := New(jenkinsClient, fakeClient, log.Log, jenkins, configurationType, emptyCustomization)
|
||||
|
||||
// when
|
||||
requeue, err := groovyClient.EnsureSingle(source, groovyScriptName, hash, groovyScript)
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
assert.True(t, requeue)
|
||||
|
||||
err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, len(jenkins.Status.AppliedGroovyScripts))
|
||||
assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[0].ConfigurationType)
|
||||
assert.Equal(t, hash, jenkins.Status.AppliedGroovyScripts[0].Hash)
|
||||
assert.Equal(t, source, jenkins.Status.AppliedGroovyScripts[0].Source)
|
||||
assert.Equal(t, groovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name)
|
||||
})
|
||||
t.Run("no execute script", func(t *testing.T) {
|
||||
// given
|
||||
jenkins := &v1alpha2.Jenkins{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: jenkinsName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Status: v1alpha2.JenkinsStatus{
|
||||
AppliedGroovyScripts: []v1alpha2.AppliedGroovyScript{
|
||||
{
|
||||
ConfigurationType: configurationType,
|
||||
Source: source,
|
||||
Name: groovyScriptName,
|
||||
Hash: hash,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme)
|
||||
require.NoError(t, err)
|
||||
fakeClient := fake.NewFakeClient()
|
||||
err = fakeClient.Create(ctx, jenkins)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
jenkinsClient := jenkinsclient.NewMockJenkins(ctrl)
|
||||
|
||||
groovyClient := New(jenkinsClient, fakeClient, log.Log, jenkins, configurationType, emptyCustomization)
|
||||
|
||||
// when
|
||||
requeue, err := groovyClient.EnsureSingle(source, groovyScriptName, hash, groovyScript)
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
assert.False(t, requeue)
|
||||
|
||||
err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, len(jenkins.Status.AppliedGroovyScripts))
|
||||
assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[0].ConfigurationType)
|
||||
assert.Equal(t, hash, jenkins.Status.AppliedGroovyScripts[0].Hash)
|
||||
assert.Equal(t, source, jenkins.Status.AppliedGroovyScripts[0].Source)
|
||||
assert.Equal(t, groovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name)
|
||||
})
|
||||
t.Run("execute script with new version", func(t *testing.T) {
|
||||
anotherHash := "hash1"
|
||||
// given
|
||||
jenkins := &v1alpha2.Jenkins{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: jenkinsName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
}
|
||||
err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme)
|
||||
require.NoError(t, err)
|
||||
fakeClient := fake.NewFakeClient()
|
||||
err = fakeClient.Create(ctx, jenkins)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
jenkinsClient := jenkinsclient.NewMockJenkins(ctrl)
|
||||
|
||||
jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil)
|
||||
jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil)
|
||||
jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil)
|
||||
|
||||
groovyClient := New(jenkinsClient, fakeClient, log.Log, jenkins, configurationType, emptyCustomization)
|
||||
|
||||
// when
|
||||
requeue, err := groovyClient.EnsureSingle(source, groovyScriptName, hash, groovyScript)
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
assert.True(t, requeue)
|
||||
err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, len(jenkins.Status.AppliedGroovyScripts))
|
||||
assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[0].ConfigurationType)
|
||||
assert.Equal(t, hash, jenkins.Status.AppliedGroovyScripts[0].Hash)
|
||||
assert.Equal(t, source, jenkins.Status.AppliedGroovyScripts[0].Source)
|
||||
assert.Equal(t, groovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name)
|
||||
|
||||
// Update with new hash
|
||||
requeue, err = groovyClient.EnsureSingle(source, groovyScriptName, anotherHash, groovyScript)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, requeue)
|
||||
|
||||
err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, len(jenkins.Status.AppliedGroovyScripts))
|
||||
assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[0].ConfigurationType)
|
||||
assert.Equal(t, anotherHash, jenkins.Status.AppliedGroovyScripts[0].Hash)
|
||||
assert.Equal(t, source, jenkins.Status.AppliedGroovyScripts[0].Source)
|
||||
assert.Equal(t, groovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name)
|
||||
|
||||
requeue, err = groovyClient.EnsureSingle(source, groovyScriptName, hash, groovyScript)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, requeue)
|
||||
|
||||
err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, len(jenkins.Status.AppliedGroovyScripts))
|
||||
assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[0].ConfigurationType)
|
||||
assert.Equal(t, hash, jenkins.Status.AppliedGroovyScripts[0].Hash)
|
||||
assert.Equal(t, source, jenkins.Status.AppliedGroovyScripts[0].Source)
|
||||
assert.Equal(t, groovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name)
|
||||
})
|
||||
t.Run("execute three groovy scripts with another hash", func(t *testing.T) {
|
||||
// given
|
||||
firstGroovyScriptName := "testGroovy1"
|
||||
firstGroovyScriptHash := "testHash1"
|
||||
|
||||
secondGroovyScriptName := "testGroovy2"
|
||||
secondGroovyScriptHash := "testHash2"
|
||||
|
||||
thirdGroovyScriptName := "testGroovy3"
|
||||
thirdGroovyScriptHash := "testHash3"
|
||||
|
||||
jenkins := &v1alpha2.Jenkins{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: jenkinsName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
}
|
||||
err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme)
|
||||
require.NoError(t, err)
|
||||
fakeClient := fake.NewFakeClient()
|
||||
err = fakeClient.Create(ctx, jenkins)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
jenkinsClient := jenkinsclient.NewMockJenkins(ctrl)
|
||||
|
||||
jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil)
|
||||
jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil)
|
||||
jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil)
|
||||
|
||||
groovyClient := New(jenkinsClient, fakeClient, log.Log, jenkins, configurationType, emptyCustomization)
|
||||
|
||||
requeue, err := groovyClient.EnsureSingle(source, firstGroovyScriptName, firstGroovyScriptHash, groovyScript)
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
assert.True(t, requeue)
|
||||
err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, len(jenkins.Status.AppliedGroovyScripts))
|
||||
assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[0].ConfigurationType)
|
||||
assert.Equal(t, firstGroovyScriptHash, jenkins.Status.AppliedGroovyScripts[0].Hash)
|
||||
assert.Equal(t, source, jenkins.Status.AppliedGroovyScripts[0].Source)
|
||||
assert.Equal(t, firstGroovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name)
|
||||
|
||||
requeue, err = groovyClient.EnsureSingle(source, secondGroovyScriptName, secondGroovyScriptHash, groovyScript)
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
assert.True(t, requeue)
|
||||
err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, len(jenkins.Status.AppliedGroovyScripts))
|
||||
assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[1].ConfigurationType)
|
||||
assert.Equal(t, secondGroovyScriptHash, jenkins.Status.AppliedGroovyScripts[1].Hash)
|
||||
assert.Equal(t, source, jenkins.Status.AppliedGroovyScripts[1].Source)
|
||||
assert.Equal(t, secondGroovyScriptName, jenkins.Status.AppliedGroovyScripts[1].Name)
|
||||
|
||||
requeue, err = groovyClient.EnsureSingle(source, thirdGroovyScriptName, thirdGroovyScriptHash, groovyScript)
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
assert.True(t, requeue)
|
||||
err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 3, len(jenkins.Status.AppliedGroovyScripts))
|
||||
assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[2].ConfigurationType)
|
||||
assert.Equal(t, thirdGroovyScriptHash, jenkins.Status.AppliedGroovyScripts[2].Hash)
|
||||
assert.Equal(t, source, jenkins.Status.AppliedGroovyScripts[2].Source)
|
||||
assert.Equal(t, thirdGroovyScriptName, jenkins.Status.AppliedGroovyScripts[2].Name)
|
||||
|
||||
})
|
||||
t.Run("execute two groovy scripts with same names in two config maps", func(t *testing.T) {
|
||||
jenkins := &v1alpha2.Jenkins{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: jenkinsName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
}
|
||||
err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme)
|
||||
require.NoError(t, err)
|
||||
fakeClient := fake.NewFakeClient()
|
||||
err = fakeClient.Create(ctx, jenkins)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
jenkinsClient := jenkinsclient.NewMockJenkins(ctrl)
|
||||
|
||||
jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil)
|
||||
jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil)
|
||||
|
||||
groovyClient := New(jenkinsClient, fakeClient, log.Log, jenkins, configurationType, emptyCustomization)
|
||||
|
||||
requeue, err := groovyClient.EnsureSingle("test-conf1", "test.groovy", hash, groovyScript)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, requeue)
|
||||
|
||||
requeue, err = groovyClient.EnsureSingle("test-conf2", "test.groovy", "anotherHash", groovyScript)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, requeue)
|
||||
|
||||
assert.Equal(t, 2, len(jenkins.Status.AppliedGroovyScripts))
|
||||
})
|
||||
t.Run("execute two groovy scripts with different configuration types", func(t *testing.T) {
|
||||
jenkins := &v1alpha2.Jenkins{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: jenkinsName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
}
|
||||
err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme)
|
||||
require.NoError(t, err)
|
||||
fakeClient := fake.NewFakeClient()
|
||||
err = fakeClient.Create(ctx, jenkins)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
jenkinsClient := jenkinsclient.NewMockJenkins(ctrl)
|
||||
jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil)
|
||||
jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil)
|
||||
|
||||
groovyClient := New(jenkinsClient, fakeClient, log.Log, jenkins, configurationType, emptyCustomization)
|
||||
|
||||
requeue, err := groovyClient.EnsureSingle(source, "test.groovy", hash, groovyScript)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, requeue)
|
||||
|
||||
groovyClient = New(jenkinsClient, fakeClient, log.Log, jenkins, "another-test-configuration-type", emptyCustomization)
|
||||
|
||||
requeue, err = groovyClient.EnsureSingle(source, "test.groovy", "anotherHash", groovyScript)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, requeue)
|
||||
|
||||
assert.Equal(t, 2, len(jenkins.Status.AppliedGroovyScripts))
|
||||
})
|
||||
t.Run("execute script fails", func(t *testing.T) {
|
||||
// given
|
||||
jenkins := &v1alpha2.Jenkins{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: jenkinsName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
}
|
||||
err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme)
|
||||
require.NoError(t, err)
|
||||
fakeClient := fake.NewFakeClient()
|
||||
err = fakeClient.Create(ctx, jenkins)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
jenkinsClient := jenkinsclient.NewMockJenkins(ctrl)
|
||||
jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("fail logs", &jenkinsclient.GroovyScriptExecutionFailed{})
|
||||
|
||||
groovyClient := New(jenkinsClient, fakeClient, log.Log, jenkins, configurationType, emptyCustomization)
|
||||
|
||||
// when
|
||||
requeue, err := groovyClient.EnsureSingle(source, groovyScriptName, hash, groovyScript)
|
||||
|
||||
// then
|
||||
require.Error(t, err)
|
||||
assert.True(t, requeue)
|
||||
|
||||
err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, len(jenkins.Status.AppliedGroovyScripts))
|
||||
})
|
||||
}
|
||||
|
||||
func TestGroovy_Ensure(t *testing.T) {
|
||||
log.SetupLogger(true)
|
||||
configurationType := "test-conf-type"
|
||||
groovyScript := "groovy-script"
|
||||
groovyScriptName := "groovy-script-name.groovy"
|
||||
ctx := context.TODO()
|
||||
jenkinsName := "jenkins"
|
||||
namespace := "default"
|
||||
configMapName := "config-map-name"
|
||||
secretName := "secret-name"
|
||||
|
||||
allGroovyScriptsFunc := func(name string) bool {
|
||||
return true
|
||||
}
|
||||
noUpdateGroovyScript := func(groovyScript string) string {
|
||||
return groovyScript
|
||||
}
|
||||
|
||||
t.Run("select groovy files with .groovy extension", func(t *testing.T) {
|
||||
// given
|
||||
groovyScriptExtension := ".groovy"
|
||||
jenkins := &v1alpha2.Jenkins{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: jenkinsName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
}
|
||||
customization := v1alpha2.Customization{
|
||||
Configurations: []v1alpha2.ConfigMapRef{
|
||||
{
|
||||
Name: configMapName,
|
||||
},
|
||||
},
|
||||
}
|
||||
configMap := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: configMapName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Data: map[string]string{
|
||||
groovyScriptName: groovyScript,
|
||||
"to-ommit": "to-ommit",
|
||||
},
|
||||
}
|
||||
err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme)
|
||||
require.NoError(t, err)
|
||||
fakeClient := fake.NewFakeClient()
|
||||
err = fakeClient.Create(ctx, jenkins)
|
||||
require.NoError(t, err)
|
||||
err = fakeClient.Create(ctx, configMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
jenkinsClient := jenkinsclient.NewMockJenkins(ctrl)
|
||||
jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil)
|
||||
|
||||
groovyClient := New(jenkinsClient, fakeClient, log.Log, jenkins, configurationType, customization)
|
||||
onlyGroovyFilesFunc := func(name string) bool {
|
||||
return strings.HasSuffix(name, groovyScriptExtension)
|
||||
}
|
||||
|
||||
// when
|
||||
requeue, err := groovyClient.Ensure(onlyGroovyFilesFunc, noUpdateGroovyScript)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, requeue)
|
||||
requeue, err = groovyClient.Ensure(onlyGroovyFilesFunc, noUpdateGroovyScript)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, requeue)
|
||||
|
||||
// then
|
||||
err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, len(jenkins.Status.AppliedGroovyScripts))
|
||||
assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[0].ConfigurationType)
|
||||
assert.Equal(t, "qoXeeh4ia+KXhT01lYNxe+oxByDf8dfT2npP9fgzjbk=", jenkins.Status.AppliedGroovyScripts[0].Hash)
|
||||
assert.Equal(t, configMapName, jenkins.Status.AppliedGroovyScripts[0].Source)
|
||||
assert.Equal(t, groovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name)
|
||||
})
|
||||
t.Run("change groovy script", func(t *testing.T) {
|
||||
// given
|
||||
groovyScriptSuffix := "suffix"
|
||||
jenkins := &v1alpha2.Jenkins{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: jenkinsName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
}
|
||||
customization := v1alpha2.Customization{
|
||||
Configurations: []v1alpha2.ConfigMapRef{
|
||||
{
|
||||
Name: configMapName,
|
||||
},
|
||||
},
|
||||
}
|
||||
configMap := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: configMapName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Data: map[string]string{
|
||||
groovyScriptName: groovyScript,
|
||||
},
|
||||
}
|
||||
err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme)
|
||||
require.NoError(t, err)
|
||||
fakeClient := fake.NewFakeClient()
|
||||
err = fakeClient.Create(ctx, jenkins)
|
||||
require.NoError(t, err)
|
||||
err = fakeClient.Create(ctx, configMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
jenkinsClient := jenkinsclient.NewMockJenkins(ctrl)
|
||||
jenkinsClient.EXPECT().ExecuteScript(groovyScript+groovyScriptSuffix).Return("logs", nil)
|
||||
|
||||
groovyClient := New(jenkinsClient, fakeClient, log.Log, jenkins, configurationType, customization)
|
||||
updateGroovyFunc := func(groovyScript string) string {
|
||||
return groovyScript + groovyScriptSuffix
|
||||
}
|
||||
|
||||
// when
|
||||
requeue, err := groovyClient.Ensure(allGroovyScriptsFunc, updateGroovyFunc)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, requeue)
|
||||
requeue, err = groovyClient.Ensure(allGroovyScriptsFunc, updateGroovyFunc)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, requeue)
|
||||
|
||||
// then
|
||||
err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, len(jenkins.Status.AppliedGroovyScripts))
|
||||
assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[0].ConfigurationType)
|
||||
assert.Equal(t, "TgTpV3nDxMNMM93t6jgni0UHa7C+uL+D+BLcW3a7b6M=", jenkins.Status.AppliedGroovyScripts[0].Hash)
|
||||
assert.Equal(t, configMapName, jenkins.Status.AppliedGroovyScripts[0].Source)
|
||||
assert.Equal(t, groovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name)
|
||||
})
|
||||
t.Run("execute script without secret and save status", func(t *testing.T) {
|
||||
// given
|
||||
jenkins := &v1alpha2.Jenkins{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: jenkinsName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
}
|
||||
customization := v1alpha2.Customization{
|
||||
Configurations: []v1alpha2.ConfigMapRef{
|
||||
{
|
||||
Name: configMapName,
|
||||
},
|
||||
},
|
||||
}
|
||||
configMap := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: configMapName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Data: map[string]string{
|
||||
groovyScriptName: groovyScript,
|
||||
},
|
||||
}
|
||||
err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme)
|
||||
require.NoError(t, err)
|
||||
fakeClient := fake.NewFakeClient()
|
||||
err = fakeClient.Create(ctx, jenkins)
|
||||
require.NoError(t, err)
|
||||
err = fakeClient.Create(ctx, configMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
jenkinsClient := jenkinsclient.NewMockJenkins(ctrl)
|
||||
jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil)
|
||||
|
||||
groovyClient := New(jenkinsClient, fakeClient, log.Log, jenkins, configurationType, customization)
|
||||
|
||||
// when
|
||||
requeue, err := groovyClient.Ensure(allGroovyScriptsFunc, noUpdateGroovyScript)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, requeue)
|
||||
requeue, err = groovyClient.Ensure(allGroovyScriptsFunc, noUpdateGroovyScript)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, requeue)
|
||||
|
||||
// then
|
||||
err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, len(jenkins.Status.AppliedGroovyScripts))
|
||||
assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[0].ConfigurationType)
|
||||
assert.Equal(t, "qoXeeh4ia+KXhT01lYNxe+oxByDf8dfT2npP9fgzjbk=", jenkins.Status.AppliedGroovyScripts[0].Hash)
|
||||
assert.Equal(t, configMapName, jenkins.Status.AppliedGroovyScripts[0].Source)
|
||||
assert.Equal(t, groovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name)
|
||||
})
|
||||
t.Run("execute script with secret and save status", func(t *testing.T) {
|
||||
// given
|
||||
jenkins := &v1alpha2.Jenkins{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: jenkinsName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
}
|
||||
customization := v1alpha2.Customization{
|
||||
Secret: v1alpha2.SecretRef{Name: secretName},
|
||||
Configurations: []v1alpha2.ConfigMapRef{
|
||||
{
|
||||
Name: configMapName,
|
||||
},
|
||||
},
|
||||
}
|
||||
configMap := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: configMapName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Data: map[string]string{
|
||||
groovyScriptName: groovyScript,
|
||||
},
|
||||
}
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: secretName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"SECRET_KEY": []byte("secret-value"),
|
||||
},
|
||||
}
|
||||
err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme)
|
||||
require.NoError(t, err)
|
||||
fakeClient := fake.NewFakeClient()
|
||||
err = fakeClient.Create(ctx, jenkins)
|
||||
require.NoError(t, err)
|
||||
err = fakeClient.Create(ctx, secret)
|
||||
require.NoError(t, err)
|
||||
err = fakeClient.Create(ctx, configMap)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
jenkinsClient := jenkinsclient.NewMockJenkins(ctrl)
|
||||
jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil)
|
||||
|
||||
groovyClient := New(jenkinsClient, fakeClient, log.Log, jenkins, configurationType, customization)
|
||||
|
||||
// when
|
||||
requeue, err := groovyClient.Ensure(allGroovyScriptsFunc, noUpdateGroovyScript)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, requeue)
|
||||
requeue, err = groovyClient.Ensure(allGroovyScriptsFunc, noUpdateGroovyScript)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, requeue)
|
||||
|
||||
// then
|
||||
err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, len(jenkins.Status.AppliedGroovyScripts))
|
||||
assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[0].ConfigurationType)
|
||||
assert.Equal(t, "em9pjw9mUheUpPRCJWD2Dww+80YQPoHCZbzzKZZw4lo=", jenkins.Status.AppliedGroovyScripts[0].Hash)
|
||||
assert.Equal(t, configMapName, jenkins.Status.AppliedGroovyScripts[0].Source)
|
||||
assert.Equal(t, groovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGroovy_isGroovyScriptAlreadyApplied(t *testing.T) {
|
||||
log.SetupLogger(true)
|
||||
emptyCustomization := v1alpha2.Customization{}
|
||||
configurationType := "test-conf-type"
|
||||
|
||||
t.Run("found", func(t *testing.T) {
|
||||
jenkins := &v1alpha2.Jenkins{
|
||||
Status: v1alpha2.JenkinsStatus{
|
||||
AppliedGroovyScripts: []v1alpha2.AppliedGroovyScript{
|
||||
{
|
||||
ConfigurationType: configurationType,
|
||||
Source: "source",
|
||||
Name: "name",
|
||||
Hash: "hash",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
groovyClient := New(nil, nil, log.Log, jenkins, configurationType, emptyCustomization)
|
||||
|
||||
got := groovyClient.isGroovyScriptAlreadyApplied("source", "name", "hash")
|
||||
|
||||
assert.True(t, got)
|
||||
})
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
jenkins := &v1alpha2.Jenkins{
|
||||
Status: v1alpha2.JenkinsStatus{
|
||||
AppliedGroovyScripts: []v1alpha2.AppliedGroovyScript{
|
||||
{
|
||||
ConfigurationType: configurationType,
|
||||
Source: "source",
|
||||
Name: "name",
|
||||
Hash: "hash",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
groovyClient := New(nil, nil, log.Log, jenkins, configurationType, emptyCustomization)
|
||||
|
||||
got := groovyClient.isGroovyScriptAlreadyApplied("source", "not-exist", "hash")
|
||||
|
||||
assert.False(t, got)
|
||||
})
|
||||
t.Run("empty Jenkins status", func(t *testing.T) {
|
||||
jenkins := &v1alpha2.Jenkins{}
|
||||
groovyClient := New(nil, nil, log.Log, jenkins, configurationType, emptyCustomization)
|
||||
|
||||
got := groovyClient.isGroovyScriptAlreadyApplied("source", "name", "hash")
|
||||
|
||||
assert.False(t, got)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAddSecretsLoaderToGroovyScript(t *testing.T) {
|
||||
secretsPath := "/var/jenkins/groovy-scripts-secrets"
|
||||
secretsLoader := fmt.Sprintf(secretsLoaderGroovyScriptFmt, secretsPath)
|
||||
|
||||
t.Run("without imports", func(t *testing.T) {
|
||||
groovyScript := "println 'Simple groovy script"
|
||||
updater := AddSecretsLoaderToGroovyScript(secretsPath)
|
||||
|
||||
got := updater(groovyScript)
|
||||
|
||||
assert.Equal(t, secretsLoader+groovyScript, got)
|
||||
})
|
||||
t.Run("with imports", func(t *testing.T) {
|
||||
groovyScript := `import com.foo.bar
|
||||
import com.foo.bar2
|
||||
println 'Simple groovy script'`
|
||||
imports := `import com.foo.bar
|
||||
import com.foo.bar2`
|
||||
tail := `println 'Simple groovy script'`
|
||||
update := AddSecretsLoaderToGroovyScript(secretsPath)
|
||||
|
||||
got := update(groovyScript)
|
||||
|
||||
assert.Equal(t, imports+"\n\n"+secretsLoader+"\n\n"+tail, got)
|
||||
})
|
||||
t.Run("with imports and separate section", func(t *testing.T) {
|
||||
groovyScript := `import com.foo.bar
|
||||
import com.foo.bar2
|
||||
|
||||
println 'Simple groovy script'`
|
||||
imports := `import com.foo.bar
|
||||
import com.foo.bar2`
|
||||
tail := `println 'Simple groovy script'`
|
||||
update := AddSecretsLoaderToGroovyScript(secretsPath)
|
||||
|
||||
got := update(groovyScript)
|
||||
|
||||
assert.Equal(t, imports+"\n\n"+secretsLoader+"\n\n\n"+tail, got)
|
||||
})
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"reflect"
|
||||
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
|
||||
jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client"
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base"
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources"
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/user"
|
||||
|
|
@ -171,6 +172,9 @@ func (r *ReconcileJenkins) Reconcile(request reconcile.Request) (reconcile.Resul
|
|||
if err == jobs.ErrorUnrecoverableBuildFailed {
|
||||
return reconcile.Result{Requeue: false}, nil
|
||||
}
|
||||
if _, ok := err.(*jenkinsclient.GroovyScriptExecutionFailed); ok {
|
||||
return reconcile.Result{Requeue: false}, nil
|
||||
}
|
||||
return reconcile.Result{Requeue: true}, nil
|
||||
}
|
||||
return result, nil
|
||||
|
|
@ -345,10 +349,6 @@ func (r *ReconcileJenkins) setDefaults(jenkins *v1alpha2.Jenkins, logger logr.Lo
|
|||
changed = true
|
||||
jenkins.Status.OperatorVersion = version.Version
|
||||
}
|
||||
if len(jenkins.Spec.Master.Plugins) == 0 {
|
||||
changed = true
|
||||
jenkins.Spec.Master.Plugins = []v1alpha2.Plugin{{Name: "simple-theme-plugin", Version: "0.5.1"}}
|
||||
}
|
||||
if isResourceRequirementsNotSet(jenkinsContainer.Resources) {
|
||||
logger.Info("Setting default Jenkins master container resource requirements")
|
||||
changed = true
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ func (p Plugin) String() string {
|
|||
}
|
||||
|
||||
var (
|
||||
namePattern = regexp.MustCompile(`^[0-9a-z-]+$`)
|
||||
namePattern = regexp.MustCompile(`^[0-9a-z-_]+$`)
|
||||
versionPattern = regexp.MustCompile(`^[0-9\\.]+$`)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,18 @@ func TestVerifyDependencies(t *testing.T) {
|
|||
got := VerifyDependencies(basePlugins)
|
||||
assert.Equal(t, true, got)
|
||||
})
|
||||
t.Run("happy, two plugin names with names with underscores", func(t *testing.T) {
|
||||
basePlugins := map[Plugin][]Plugin{
|
||||
Must(New("first_root_plugin:1.0.0")): {
|
||||
Must(New("first_plugin:0.0.1")),
|
||||
},
|
||||
Must(New("second_root_plugin:1.0.0")): {
|
||||
Must(New("first_plugin:0.0.1")),
|
||||
},
|
||||
}
|
||||
got := VerifyDependencies(basePlugins)
|
||||
assert.Equal(t, true, got)
|
||||
})
|
||||
t.Run("fail, two root plugins have different versions", func(t *testing.T) {
|
||||
basePlugins := map[Plugin][]Plugin{
|
||||
Must(New("first-root-plugin:1.0.0")): {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client"
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base"
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources"
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/groovy"
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/plugins"
|
||||
|
||||
"github.com/bndr/gojenkins"
|
||||
|
|
@ -27,6 +28,7 @@ func TestConfiguration(t *testing.T) {
|
|||
|
||||
jenkinsCRName := "e2e"
|
||||
numberOfExecutors := 6
|
||||
numberOfExecutorsEnvName := "NUMBER_OF_EXECUTORS"
|
||||
systemMessage := "Configuration as Code integration works!!!"
|
||||
systemMessageEnvName := "SYSTEM_MESSAGE"
|
||||
mySeedJob := seedJobConfig{
|
||||
|
|
@ -40,35 +42,43 @@ func TestConfiguration(t *testing.T) {
|
|||
RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git",
|
||||
},
|
||||
}
|
||||
volumes := []corev1.Volume{
|
||||
{
|
||||
Name: "test-configmap",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
ConfigMap: &corev1.ConfigMapVolumeSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: resources.GetUserConfigurationConfigMapName(jenkinsCRName),
|
||||
},
|
||||
groovyScripts := v1alpha2.GroovyScripts{
|
||||
Customization: v1alpha2.Customization{
|
||||
Configurations: []v1alpha2.ConfigMapRef{
|
||||
{
|
||||
Name: userConfigurationConfigMapName,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "test-secret",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
Secret: &corev1.SecretVolumeSource{
|
||||
SecretName: resources.GetUserConfigurationSecretName(jenkinsCRName),
|
||||
},
|
||||
Secret: v1alpha2.SecretRef{
|
||||
Name: userConfigurationSecretName,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
casc := v1alpha2.ConfigurationAsCode{
|
||||
Customization: v1alpha2.Customization{
|
||||
Configurations: []v1alpha2.ConfigMapRef{
|
||||
{
|
||||
Name: userConfigurationConfigMapName,
|
||||
},
|
||||
},
|
||||
Secret: v1alpha2.SecretRef{
|
||||
Name: userConfigurationSecretName,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
stringData := make(map[string]string)
|
||||
stringData[systemMessageEnvName] = systemMessage
|
||||
stringData[numberOfExecutorsEnvName] = fmt.Sprintf("%d", numberOfExecutors)
|
||||
|
||||
// base
|
||||
createUserConfigurationSecret(t, jenkinsCRName, namespace, systemMessageEnvName, systemMessage)
|
||||
createUserConfigurationConfigMap(t, jenkinsCRName, namespace, numberOfExecutors, fmt.Sprintf("${%s}", systemMessageEnvName))
|
||||
jenkins := createJenkinsCR(t, jenkinsCRName, namespace, &[]v1alpha2.SeedJob{mySeedJob.SeedJob}, volumes)
|
||||
createUserConfigurationSecret(t, namespace, stringData)
|
||||
createUserConfigurationConfigMap(t, namespace, numberOfExecutorsEnvName, fmt.Sprintf("${%s}", systemMessageEnvName))
|
||||
jenkins := createJenkinsCR(t, jenkinsCRName, namespace, &[]v1alpha2.SeedJob{mySeedJob.SeedJob}, groovyScripts, casc)
|
||||
createDefaultLimitsForContainersInNamespace(t, namespace)
|
||||
createKubernetesCredentialsProviderSecret(t, namespace, mySeedJob)
|
||||
waitForJenkinsBaseConfigurationToComplete(t, jenkins)
|
||||
|
||||
verifyJenkinsMasterPodAttributes(t, jenkins)
|
||||
client := verifyJenkinsAPIConnection(t, jenkins)
|
||||
verifyPlugins(t, client, jenkins)
|
||||
|
|
@ -79,15 +89,13 @@ func TestConfiguration(t *testing.T) {
|
|||
verifyJenkinsSeedJobs(t, client, []seedJobConfig{mySeedJob})
|
||||
}
|
||||
|
||||
func createUserConfigurationSecret(t *testing.T, jenkinsCRName string, namespace string, systemMessageEnvName, systemMessage string) {
|
||||
func createUserConfigurationSecret(t *testing.T, namespace string, stringData map[string]string) {
|
||||
userConfiguration := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: resources.GetUserConfigurationSecretName(jenkinsCRName),
|
||||
Name: userConfigurationSecretName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
StringData: map[string]string{
|
||||
systemMessageEnvName: systemMessage,
|
||||
},
|
||||
StringData: stringData,
|
||||
}
|
||||
|
||||
t.Logf("User configuration secret %+v", *userConfiguration)
|
||||
|
|
@ -96,18 +104,18 @@ func createUserConfigurationSecret(t *testing.T, jenkinsCRName string, namespace
|
|||
}
|
||||
}
|
||||
|
||||
func createUserConfigurationConfigMap(t *testing.T, jenkinsCRName string, namespace string, numberOfExecutors int, systemMessage string) {
|
||||
func createUserConfigurationConfigMap(t *testing.T, namespace string, numberOfExecutorsSecretKeyName string, systemMessage string) {
|
||||
userConfiguration := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: resources.GetUserConfigurationConfigMapName(jenkinsCRName),
|
||||
Name: userConfigurationConfigMapName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Data: map[string]string{
|
||||
"1-set-executors.groovy": fmt.Sprintf(`
|
||||
import jenkins.model.Jenkins
|
||||
|
||||
Jenkins.instance.setNumExecutors(%d)
|
||||
Jenkins.instance.save()`, numberOfExecutors),
|
||||
Jenkins.instance.setNumExecutors(new Integer(secrets['%s']))
|
||||
Jenkins.instance.save()`, numberOfExecutorsSecretKeyName),
|
||||
"1-casc.yaml": fmt.Sprintf(`
|
||||
jenkins:
|
||||
systemMessage: "%s"`, systemMessage),
|
||||
|
|
@ -163,6 +171,11 @@ func verifyJenkinsMasterPodAttributes(t *testing.T, jenkins *v1alpha2.Jenkins) {
|
|||
assert.Equal(t, resources.JenkinsMasterContainerName, jenkinsPod.Spec.Containers[0].Name)
|
||||
assert.Equal(t, len(jenkins.Spec.Master.Containers), len(jenkinsPod.Spec.Containers))
|
||||
|
||||
assert.Equal(t, jenkins.Spec.Master.SecurityContext, jenkinsPod.Spec.SecurityContext)
|
||||
assert.Equal(t, jenkins.Spec.Master.Containers[0].Command, jenkinsPod.Spec.Containers[0].Command)
|
||||
|
||||
assert.Equal(t, jenkins.Spec.Master.ImagePullSecrets, jenkinsPod.Spec.ImagePullSecrets)
|
||||
|
||||
for _, actualContainer := range jenkinsPod.Spec.Containers {
|
||||
if actualContainer.Name == resources.JenkinsMasterContainerName {
|
||||
verifyContainer(t, resources.NewJenkinsMasterContainer(jenkins), actualContainer)
|
||||
|
|
@ -265,6 +278,15 @@ if (!new Integer(%d).equals(Jenkins.instance.numExecutors)) {
|
|||
logs, err := jenkinsClient.ExecuteScript(checkConfigurationViaGroovyScript)
|
||||
assert.NoError(t, err, logs)
|
||||
|
||||
checkSecretLoaderViaGroovyScript := fmt.Sprintf(`
|
||||
if (!new Integer(%d).equals(new Integer(secrets['NUMBER_OF_EXECUTORS']))) {
|
||||
throw new Exception("Secret not found by given key: NUMBER_OF_EXECUTORS")
|
||||
}`, amountOfExecutors)
|
||||
|
||||
loader := groovy.AddSecretsLoaderToGroovyScript("/var/jenkins/groovy-scripts-secrets")
|
||||
logs, err = jenkinsClient.ExecuteScript(loader(checkSecretLoaderViaGroovyScript))
|
||||
assert.NoError(t, err, logs)
|
||||
|
||||
checkConfigurationAsCode := fmt.Sprintf(`
|
||||
if (!"%s".equals(Jenkins.instance.systemMessage)) {
|
||||
throw new Exception("Configuration as code failed")
|
||||
|
|
|
|||
|
|
@ -17,6 +17,11 @@ import (
|
|||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
)
|
||||
|
||||
const (
|
||||
userConfigurationConfigMapName = "user-config"
|
||||
userConfigurationSecretName = "user-secret"
|
||||
)
|
||||
|
||||
func getJenkins(t *testing.T, namespace, name string) *v1alpha2.Jenkins {
|
||||
jenkins := &v1alpha2.Jenkins{}
|
||||
namespaceName := types.NamespacedName{Namespace: namespace, Name: name}
|
||||
|
|
@ -60,7 +65,7 @@ func createJenkinsAPIClient(jenkins *v1alpha2.Jenkins) (jenkinsclient.Jenkins, e
|
|||
)
|
||||
}
|
||||
|
||||
func createJenkinsCR(t *testing.T, name, namespace string, seedJob *[]v1alpha2.SeedJob, volumes []corev1.Volume) *v1alpha2.Jenkins {
|
||||
func createJenkinsCR(t *testing.T, name, namespace string, seedJob *[]v1alpha2.SeedJob, groovyScripts v1alpha2.GroovyScripts, casc v1alpha2.ConfigurationAsCode) *v1alpha2.Jenkins {
|
||||
var seedJobs []v1alpha2.SeedJob
|
||||
if seedJob != nil {
|
||||
seedJobs = append(seedJobs, *seedJob...)
|
||||
|
|
@ -73,6 +78,8 @@ func createJenkinsCR(t *testing.T, name, namespace string, seedJob *[]v1alpha2.S
|
|||
Namespace: namespace,
|
||||
},
|
||||
Spec: v1alpha2.JenkinsSpec{
|
||||
GroovyScripts: groovyScripts,
|
||||
ConfigurationAsCode: casc,
|
||||
Master: v1alpha2.JenkinsMaster{
|
||||
Annotations: map[string]string{"test": "label"},
|
||||
Containers: []v1alpha2.Container{
|
||||
|
|
@ -119,7 +126,6 @@ func createJenkinsCR(t *testing.T, name, namespace string, seedJob *[]v1alpha2.S
|
|||
{Name: "simple-theme-plugin", Version: "0.5.1"},
|
||||
},
|
||||
NodeSelector: map[string]string{"kubernetes.io/hostname": "minikube"},
|
||||
Volumes: volumes,
|
||||
},
|
||||
SeedJobs: seedJobs,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import (
|
|||
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
|
||||
jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client"
|
||||
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources"
|
||||
|
||||
framework "github.com/operator-framework/operator-sdk/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
|
@ -21,7 +20,7 @@ func TestJenkinsMasterPodRestart(t *testing.T) {
|
|||
// Deletes test namespace
|
||||
defer ctx.Cleanup()
|
||||
|
||||
jenkins := createJenkinsCR(t, "e2e", namespace, nil, []corev1.Volume{})
|
||||
jenkins := createJenkinsCR(t, "e2e", namespace, nil, v1alpha2.GroovyScripts{}, v1alpha2.ConfigurationAsCode{})
|
||||
waitForJenkinsBaseConfigurationToComplete(t, jenkins)
|
||||
restartJenkinsMasterPod(t, jenkins)
|
||||
waitForRecreateJenkinsMasterPod(t, jenkins)
|
||||
|
|
@ -36,8 +35,8 @@ func TestSafeRestart(t *testing.T) {
|
|||
defer ctx.Cleanup()
|
||||
|
||||
jenkinsCRName := "e2e"
|
||||
configureAuthorizationToUnSecure(t, jenkinsCRName, namespace)
|
||||
jenkins := createJenkinsCR(t, jenkinsCRName, namespace, nil, []corev1.Volume{})
|
||||
configureAuthorizationToUnSecure(t, namespace)
|
||||
jenkins := createJenkinsCR(t, jenkinsCRName, namespace, nil, v1alpha2.GroovyScripts{}, v1alpha2.ConfigurationAsCode{})
|
||||
waitForJenkinsBaseConfigurationToComplete(t, jenkins)
|
||||
waitForJenkinsUserConfigurationToComplete(t, jenkins)
|
||||
jenkinsClient := verifyJenkinsAPIConnection(t, jenkins)
|
||||
|
|
@ -50,10 +49,10 @@ func TestSafeRestart(t *testing.T) {
|
|||
checkIfAuthorizationStrategyUnsecuredIsSet(t, jenkinsClient)
|
||||
}
|
||||
|
||||
func configureAuthorizationToUnSecure(t *testing.T, jenkinsCRName, namespace string) {
|
||||
func configureAuthorizationToUnSecure(t *testing.T, namespace string) {
|
||||
limitRange := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: resources.GetUserConfigurationConfigMapName(jenkinsCRName),
|
||||
Name: userConfigurationConfigMapName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Data: map[string]string{
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ func TestSeedJobs(t *testing.T) {
|
|||
createKubernetesCredentialsProviderSecret(t, namespace, seedJobConfig)
|
||||
seedJobs = append(seedJobs, seedJobConfig.SeedJob)
|
||||
}
|
||||
jenkins := createJenkinsCR(t, jenkinsCRName, namespace, &seedJobs, []corev1.Volume{})
|
||||
jenkins := createJenkinsCR(t, jenkinsCRName, namespace, &seedJobs, v1alpha2.GroovyScripts{}, v1alpha2.ConfigurationAsCode{})
|
||||
waitForJenkinsBaseConfigurationToComplete(t, jenkins)
|
||||
|
||||
verifyJenkinsMasterPodAttributes(t, jenkins)
|
||||
|
|
|
|||
Loading…
Reference in New Issue