diff --git a/Makefile b/Makefile index 33dab1655..658928eb1 100644 --- a/Makefile +++ b/Makefile @@ -97,7 +97,7 @@ e2e: deepcopy-gen manifests ## Runs e2e tests, you can use EXTRA_ARGS .PHONY: helm-e2e IMAGE_NAME := $(DOCKER_REGISTRY):$(GITCOMMIT) #TODO: install cert-manager before running helm charts -helm-e2e: helm install-cert-manager container-runtime-build ## Runs helm e2e tests, you can use EXTRA_ARGS +helm-e2e: helm container-runtime-build ## Runs helm e2e tests, you can use EXTRA_ARGS @echo "+ $@" RUNNING_TESTS=1 go test -parallel=1 "./test/helm/" -ginkgo.v -tags "$(BUILDTAGS) cgo" -v -timeout 60m -run "$(E2E_TEST_SELECTOR)" -image-name=$(IMAGE_NAME) $(E2E_TEST_ARGS) @@ -538,8 +538,10 @@ all-in-one-build-webhook: ## Re-generate all-in-one yaml # start the cluster locally and set it to use the docker daemon from minikube install-cert-manager: minikube-start - kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.4.0/cert-manager.yaml + kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.5.1/cert-manager.yaml +uninstall-cert-manager: minikube-start + kubectl delete -f https://github.com/jetstack/cert-manager/releases/download/v1.5.1/cert-manager.yaml #Launch cert-manager and deploy the operator locally along with webhook deploy-webhook: install-cert-manager install-crds container-runtime-build all-in-one-build-webhook diff --git a/api/v1alpha2/jenkins_webhook.go b/api/v1alpha2/jenkins_webhook.go index 1fe68a5db..8d340b464 100644 --- a/api/v1alpha2/jenkins_webhook.go +++ b/api/v1alpha2/jenkins_webhook.go @@ -26,6 +26,7 @@ import ( "os" "time" + //"github.com/jenkinsci/kubernetes-operator/pkg/configuration/base/resources" "github.com/jenkinsci/kubernetes-operator/pkg/constants" "github.com/jenkinsci/kubernetes-operator/pkg/plugins" @@ -345,15 +346,32 @@ func CreateJenkinsCR(name string, namespace string, userPlugins []Plugin, valida Namespace: namespace, }, Spec: JenkinsSpec{ + GroovyScripts: GroovyScripts{ + Customization: Customization{ + Configurations: []ConfigMapRef{}, + Secret: SecretRef{ + Name: "", + }, + }, + }, + ConfigurationAsCode: ConfigurationAsCode{ + Customization: Customization{ + Configurations: []ConfigMapRef{}, + Secret: SecretRef{ + Name: "", + }, + }, + }, Master: JenkinsMaster{ - Annotations: map[string]string{"test": "label"}, - Plugins: userPlugins, + Plugins: userPlugins, + DisableCSRFProtection: false, }, ValidateSecurityWarnings: validateSecurityWarnings, Service: Service{ Type: corev1.ServiceTypeNodePort, Port: constants.DefaultHTTPPortInt32, }, + JenkinsAPISettings: JenkinsAPISettings{AuthorizationStrategy: CreateUserAuthorizationStrategy}, }, } diff --git a/chart/jenkins-operator/templates/jenkins.yaml b/chart/jenkins-operator/templates/jenkins.yaml index 9ba138b0f..aae8f6acd 100644 --- a/chart/jenkins-operator/templates/jenkins.yaml +++ b/chart/jenkins-operator/templates/jenkins.yaml @@ -145,8 +145,8 @@ spec: securityContext: {{- toYaml . | nindent 6 }} {{- end }} - {{- with .Values.jenkins.seedJobs }} ValidateSecurityWarnings: {{ .Values.jenkins.ValidateSecurityWarnings }} + {{- with .Values.jenkins.seedJobs }} seedJobs: {{- toYaml . | nindent 4 }} {{- end }} {{- end }} diff --git a/chart/jenkins-operator/templates/webhook-certificates.yaml b/chart/jenkins-operator/templates/webhook-certificates.yaml index 3edc1045c..cb87becf7 100644 --- a/chart/jenkins-operator/templates/webhook-certificates.yaml +++ b/chart/jenkins-operator/templates/webhook-certificates.yaml @@ -25,4 +25,10 @@ spec: selfSigned: {} --- +apiVersion: v1 +kind: Secret +metadata: + name: jenkins-{{ .Values.webhook.certificate.name }} +type: opaque + {{- end }} \ No newline at end of file diff --git a/chart/jenkins-operator/templates/webhook.yaml b/chart/jenkins-operator/templates/webhook.yaml index 69f07693c..bf31ce5e4 100644 --- a/chart/jenkins-operator/templates/webhook.yaml +++ b/chart/jenkins-operator/templates/webhook.yaml @@ -16,6 +16,7 @@ webhooks: path: /validate-jenkins-io-v1alpha2-jenkins failurePolicy: Fail name: vjenkins.kb.io + timeoutSeconds: 30 rules: - apiGroups: - jenkins.io diff --git a/chart/jenkins-operator/values.yaml b/chart/jenkins-operator/values.yaml index 531a3098a..ce3fb1c8f 100644 --- a/chart/jenkins-operator/values.yaml +++ b/chart/jenkins-operator/values.yaml @@ -292,4 +292,10 @@ webhook: # time after which the certificate will be automatically renewed renewbefore: 360h # enable or disable the validation webhook - enabled: false \ No newline at end of file + enabled: false + +# This startupapicheck is a Helm post-install hook that waits for the webhook +# endpoints to become available. +cert-manager: + startupapicheck: + enabled: false \ No newline at end of file diff --git a/config/crd/bases/jenkins.io_jenkins.yaml b/config/crd/bases/jenkins.io_jenkins.yaml index 47d626c6e..953509023 100644 --- a/config/crd/bases/jenkins.io_jenkins.yaml +++ b/config/crd/bases/jenkins.io_jenkins.yaml @@ -5,6 +5,7 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.4.1 + creationTimestamp: null name: jenkins.jenkins.io spec: group: jenkins.io diff --git a/main.go b/main.go index 543edad1e..8a6fe1ba4 100644 --- a/main.go +++ b/main.go @@ -180,8 +180,10 @@ func main() { fatal(errors.Wrap(err, "unable to create Jenkins controller"), *debug) } - if err = (&v1alpha2.Jenkins{}).SetupWebhookWithManager(mgr); err != nil { - fatal(errors.Wrap(err, "unable to create Webhook"), *debug) + if ValidateSecurityWarnings { + if err = (&v1alpha2.Jenkins{}).SetupWebhookWithManager(mgr); err != nil { + fatal(errors.Wrap(err, "unable to create Webhook"), *debug) + } } // +kubebuilder:scaffold:builder diff --git a/test/helm/helm_test.go b/test/helm/helm_test.go index bc05172a9..cfdb35ab1 100644 --- a/test/helm/helm_test.go +++ b/test/helm/helm_test.go @@ -1,20 +1,26 @@ package helm import ( + "context" "fmt" "os/exec" + "time" "github.com/jenkinsci/kubernetes-operator/api/v1alpha2" + "github.com/jenkinsci/kubernetes-operator/pkg/configuration/base/resources" + "github.com/jenkinsci/kubernetes-operator/pkg/constants" "github.com/jenkinsci/kubernetes-operator/test/e2e" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" // +kubebuilder:scaffold:imports ) -var _ = Describe("Jenkins controller", func() { +var _ = Describe("Jenkins Controller with webhook", func() { + var ( namespace *corev1.Namespace ) @@ -23,38 +29,193 @@ var _ = Describe("Jenkins controller", func() { namespace = e2e.CreateNamespace() }) AfterEach(func() { + cmd := exec.Command("../../bin/helm", "delete", "jenkins", "--namespace", namespace.Name) + output, err := cmd.CombinedOutput() + Expect(err).NotTo(HaveOccurred(), string(output)) + e2e.ShowLogsIfTestHasFailed(CurrentGinkgoTestDescription().Failed, namespace.Name) e2e.DestroyNamespace(namespace) }) - Context("when deploying Helm Chart to cluster", func() { - It("creates Jenkins instance and configures it", func() { - - jenkins := &v1alpha2.Jenkins{ - TypeMeta: v1alpha2.JenkinsTypeMeta(), - ObjectMeta: metav1.ObjectMeta{ - Name: "jenkins", - Namespace: namespace.Name, - }, - } + It("Deploys Jenkins operator with webhook enabled along with the default jenkins image", func() { + jenkins := &v1alpha2.Jenkins{ + TypeMeta: v1alpha2.JenkinsTypeMeta(), + ObjectMeta: metav1.ObjectMeta{ + Name: "jenkins", + Namespace: namespace.Name, + }, + } + + cmd := exec.Command("../../bin/helm", "upgrade", "jenkins", "../../chart/jenkins-operator", "--namespace", namespace.Name, "--debug", + "--set-string", fmt.Sprintf("jenkins.namespace=%s", namespace.Name), + "--set-string", fmt.Sprintf("operator.image=%s", *imageName), "--install", "--wait") + output, err := cmd.CombinedOutput() + Expect(err).NotTo(HaveOccurred(), string(output)) + + e2e.WaitForJenkinsBaseConfigurationToComplete(jenkins) + e2e.WaitForJenkinsUserConfigurationToComplete(jenkins) + + }) - cmd := exec.Command("../../bin/helm", "upgrade", "jenkins", "../../chart/jenkins-operator", "--namespace", namespace.Name, "--debug", - "--set-string", fmt.Sprintf("jenkins.namespace=%s", namespace.Name), - "--set-string", fmt.Sprintf("operator.image=%s", *imageName), "--install") - output, err := cmd.CombinedOutput() - Expect(err).NotTo(HaveOccurred(), string(output)) + It("Deploys Jenkins operator along with webhook and cert-manager", func() { - e2e.WaitForJenkinsBaseConfigurationToComplete(jenkins) - e2e.WaitForJenkinsUserConfigurationToComplete(jenkins) + By("Deploying the operator along with webhook and cert-manager") + cmd := exec.Command("../../bin/helm", "upgrade", "jenkins", "../../chart/jenkins-operator", "--namespace", namespace.Name, "--debug", + "--set-string", fmt.Sprintf("jenkins.namespace=%s", namespace.Name), "--set-string", fmt.Sprintf("operator.image=%s", *imageName), + "--set", fmt.Sprintf("webhook.enabled=%t", true), "--set", fmt.Sprintf("jenkins.enabled=%t", false), "--install", "--wait") + output, err := cmd.CombinedOutput() + Expect(err).NotTo(HaveOccurred(), string(output)) - cmd = exec.Command("../../bin/helm", "upgrade", "jenkins", "../../chart/jenkins-operator", "--namespace", namespace.Name, "--debug", - "--set-string", fmt.Sprintf("jenkins.namespace=%s", namespace.Name), - "--set-string", fmt.Sprintf("operator.image=%s", *imageName), "--install") - output, err = cmd.CombinedOutput() + By("Waiting for the operator to fetch the plugin data ") + time.Sleep(time.Duration(200) * time.Second) - Expect(err).NotTo(HaveOccurred(), string(output)) + By("Denies a create request for a Jenkins custom resource with some plugins having security warnings and validation is turned on") + userplugins := []v1alpha2.Plugin{ + {Name: "simple-theme-plugin", Version: "0.6"}, + {Name: "audit-trail", Version: "3.5"}, + {Name: "github", Version: "1.29.0"}, + } + jenkins := CreateJenkinsCR("jenkins", namespace.Name, userplugins, true) + Expect(e2e.K8sClient.Create(context.TODO(), jenkins)).Should(MatchError("admission webhook \"vjenkins.kb.io\" denied the request: security vulnerabilities detected in the following user-defined plugins: \naudit-trail:3.5\ngithub:1.29.0")) - e2e.WaitForJenkinsBaseConfigurationToComplete(jenkins) - e2e.WaitForJenkinsUserConfigurationToComplete(jenkins) - }) + By("Creating the same Jenkins custom resource with some plugins having security warnings but validation is turned off") + userplugins = []v1alpha2.Plugin{ + {Name: "simple-theme-plugin", Version: "0.6"}, + {Name: "audit-trail", Version: "3.5"}, + {Name: "github", Version: "1.31.0"}, + } + jenkins = CreateJenkinsCR("jenkins", namespace.Name, userplugins, false) + Expect(e2e.K8sClient.Create(context.TODO(), jenkins)).Should(Succeed()) + e2e.WaitForJenkinsBaseConfigurationToComplete(jenkins) + e2e.WaitForJenkinsUserConfigurationToComplete(jenkins) + + By("Updating the Jenkins resource with plugins not having any security warnings and validation is turned on") + userplugins = []v1alpha2.Plugin{ + {Name: "simple-theme-plugin", Version: "0.6"}, + {Name: "audit-trail", Version: "3.8"}, + {Name: "github", Version: "2.9"}, + } + jenkins.Spec.Master.Plugins = userplugins + jenkins.Spec.ValidateSecurityWarnings = true + Expect(e2e.K8sClient.Update(context.TODO(), jenkins)).Should(Succeed()) + jenkins = &v1alpha2.Jenkins{ + TypeMeta: v1alpha2.JenkinsTypeMeta(), + ObjectMeta: metav1.ObjectMeta{ + Name: "jenkins", + Namespace: namespace.Name, + }, + } + e2e.WaitForJenkinsBaseConfigurationToComplete(jenkins) + e2e.WaitForJenkinsUserConfigurationToComplete(jenkins) + + By("Failing to update the Jenkins custom resource because some plugins having security warnings and validation is turned on") + userplugins = []v1alpha2.Plugin{ + {Name: "vncviewer", Version: "1.7"}, + {Name: "build-timestamp", Version: "1.0.3"}, + {Name: "deployit-plugin", Version: "7.5.5"}, + {Name: "github-branch-source", Version: "2.0.7"}, + {Name: "aws-lambda-cloud", Version: "0.4"}, + {Name: "groovy", Version: "1.31"}, + {Name: "google-login", Version: "1.2"}, + } + jenkins.Spec.Master.Plugins = userplugins + Expect(e2e.K8sClient.Update(context.TODO(), jenkins)).Should(MatchError("admission webhook \"vjenkins.kb.io\" denied the request: security vulnerabilities detected in the following user-defined plugins: \nvncviewer:1.7\ndeployit-plugin:7.5.5\ngithub-branch-source:2.0.7\ngroovy:1.31\ngoogle-login:1.2")) }) }) + +func CreateJenkinsCR(name string, namespace string, userPlugins []v1alpha2.Plugin, validateSecurityWarnings bool) *v1alpha2.Jenkins { + jenkins := &v1alpha2.Jenkins{ + TypeMeta: v1alpha2.JenkinsTypeMeta(), + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: v1alpha2.JenkinsSpec{ + GroovyScripts: v1alpha2.GroovyScripts{ + Customization: v1alpha2.Customization{ + Configurations: []v1alpha2.ConfigMapRef{}, + Secret: v1alpha2.SecretRef{ + Name: "", + }, + }, + }, + ConfigurationAsCode: v1alpha2.ConfigurationAsCode{ + Customization: v1alpha2.Customization{ + Configurations: []v1alpha2.ConfigMapRef{}, + Secret: v1alpha2.SecretRef{ + Name: "", + }, + }, + }, + Master: v1alpha2.JenkinsMaster{ + Containers: []v1alpha2.Container{ + { + Name: resources.JenkinsMasterContainerName, + Env: []corev1.EnvVar{ + { + Name: "TEST_ENV", + Value: "test_env_value", + }, + }, + ReadinessProbe: &corev1.Probe{ + Handler: corev1.Handler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/login", + Port: intstr.FromString("http"), + Scheme: corev1.URISchemeHTTP, + }, + }, + InitialDelaySeconds: int32(100), + TimeoutSeconds: int32(4), + FailureThreshold: int32(40), + SuccessThreshold: int32(1), + PeriodSeconds: int32(10), + }, + LivenessProbe: &corev1.Probe{ + Handler: corev1.Handler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/login", + Port: intstr.FromString("http"), + Scheme: corev1.URISchemeHTTP, + }, + }, + InitialDelaySeconds: int32(80), + TimeoutSeconds: int32(4), + FailureThreshold: int32(30), + SuccessThreshold: int32(1), + PeriodSeconds: int32(5), + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "plugins-cache", + MountPath: "/usr/share/jenkins/ref/plugins", + }, + }, + }, + { + Name: "envoyproxy", + Image: "envoyproxy/envoy-alpine:v1.14.1", + }, + }, + Plugins: userPlugins, + DisableCSRFProtection: false, + NodeSelector: map[string]string{"kubernetes.io/os": "linux"}, + Volumes: []corev1.Volume{ + { + Name: "plugins-cache", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + }, + }, + ValidateSecurityWarnings: validateSecurityWarnings, + Service: v1alpha2.Service{ + Type: corev1.ServiceTypeNodePort, + Port: constants.DefaultHTTPPortInt32, + }, + JenkinsAPISettings: v1alpha2.JenkinsAPISettings{AuthorizationStrategy: v1alpha2.CreateUserAuthorizationStrategy}, + }, + } + + return jenkins +}