update
diff --git a/charts/jenkins/templates/NOTES.txt b/charts/jenkins/templates/NOTES.txt
new file mode 100644
index 0000000..953dd26
--- /dev/null
+++ b/charts/jenkins/templates/NOTES.txt
@@ -0,0 +1,68 @@
+{{- $prefix := .Values.controller.jenkinsUriPrefix | default "" -}}
+{{- $url := "" -}}
+1. Get your '{{ .Values.controller.admin.username }}' user password by running:
+  kubectl exec --namespace {{ template "jenkins.namespace" . }} -it svc/{{ template "jenkins.fullname" . }} -c jenkins -- /bin/cat /run/secrets/additional/chart-admin-password && echo
+{{- if .Values.controller.ingress.hostName -}}
+{{- if .Values.controller.ingress.tls -}}
+{{- $url = print "https://" .Values.controller.ingress.hostName $prefix -}}
+{{- else -}}
+{{- $url = print "http://" .Values.controller.ingress.hostName $prefix -}}
+{{- end }}
+2. Visit {{ $url }}
+{{- else }}
+2. Get the Jenkins URL to visit by running these commands in the same shell:
+{{- if contains "NodePort" .Values.controller.serviceType }}
+  export NODE_PORT=$(kubectl get --namespace {{ template "jenkins.namespace" . }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ template "jenkins.fullname" . }})
+  export NODE_IP=$(kubectl get nodes --namespace {{ template "jenkins.namespace" . }} -o jsonpath="{.items[0].status.addresses[0].address}")
+{{- if .Values.controller.httpsKeyStore.enable -}}
+{{- $url = print "https://$NODE_IP:$NODE_PORT" $prefix -}}
+{{- else -}}
+{{- $url = print "http://$NODE_IP:$NODE_PORT" $prefix -}}
+{{- end }}
+  echo {{ $url }}
+
+{{- else if contains "LoadBalancer" .Values.controller.serviceType }}
+  NOTE: It may take a few minutes for the LoadBalancer IP to be available.
+        You can watch the status of by running 'kubectl get svc --namespace {{ template "jenkins.namespace" . }} -w {{ template "jenkins.fullname" . }}'
+  export SERVICE_IP=$(kubectl get svc --namespace {{ template "jenkins.namespace" . }} {{ template "jenkins.fullname" . }} --template "{{ "{{ range (index .status.loadBalancer.ingress 0) }}{{ . }}{{ end }}" }}")
+{{- if .Values.controller.httpsKeyStore.enable -}}
+{{- $url = print "https://$SERVICE_IP:" .Values.controller.servicePort $prefix -}}
+{{- else -}}
+{{- $url = print "http://$SERVICE_IP:" .Values.controller.servicePort $prefix -}}
+{{- end }}
+  echo {{ $url }}
+
+{{- else if contains "ClusterIP"  .Values.controller.serviceType -}}
+{{- if .Values.controller.httpsKeyStore.enable -}}
+{{- $url = print "https://127.0.0.1:" .Values.controller.servicePort $prefix -}}
+{{- else -}}
+{{- $url = print "http://127.0.0.1:" .Values.controller.servicePort $prefix -}}
+{{- end }}
+  echo {{ $url }}
+  kubectl --namespace {{ template "jenkins.namespace" . }} port-forward svc/{{template "jenkins.fullname" . }} {{ .Values.controller.servicePort }}:{{ .Values.controller.servicePort }}
+{{- end }}
+{{- end }}
+
+3. Login with the password from step 1 and the username: {{ .Values.controller.admin.username }}
+4. Configure security realm and authorization strategy
+5. Use Jenkins Configuration as Code by specifying configScripts in your values.yaml file, see documentation: {{ $url }}/configuration-as-code and examples: https://github.com/jenkinsci/configuration-as-code-plugin/tree/master/demos
+
+For more information on running Jenkins on Kubernetes, visit:
+https://cloud.google.com/solutions/jenkins-on-container-engine
+
+For more information about Jenkins Configuration as Code, visit:
+https://jenkins.io/projects/jcasc/
+
+{{ if and (eq .Values.controller.image.repository "jenkins/jenkins") (eq .Values.controller.image.registry "docker.io") }}
+NOTE: Consider using a custom image with pre-installed plugins
+{{- else if .Values.controller.installPlugins }}
+NOTE: Consider disabling `installPlugins` if your image already contains plugins.
+{{- end }}
+
+{{- if .Values.persistence.enabled }}
+{{- else }}
+#################################################################################
+######   WARNING: Persistence is disabled!!! You will lose your data when   #####
+######            the Jenkins pod is terminated.                            #####
+#################################################################################
+{{- end }}
diff --git a/charts/jenkins/templates/_helpers.tpl b/charts/jenkins/templates/_helpers.tpl
new file mode 100644
index 0000000..8301a84
--- /dev/null
+++ b/charts/jenkins/templates/_helpers.tpl
@@ -0,0 +1,655 @@
+{{/* vim: set filetype=mustache: */}}
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "jenkins.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
+
+{{/*
+Expand the label of the chart.
+*/}}
+{{- define "jenkins.label" -}}
+{{- printf "%s-%s" (include "jenkins.name" .) .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
+
+
+{{/*
+Allow the release namespace to be overridden for multi-namespace deployments in combined charts.
+*/}}
+{{- define "jenkins.namespace" -}}
+  {{- if .Values.namespaceOverride -}}
+    {{- .Values.namespaceOverride -}}
+  {{- else -}}
+    {{- .Release.Namespace -}}
+  {{- end -}}
+{{- end -}}
+
+{{- define "jenkins.agent.namespace" -}}
+  {{- if .Values.agent.namespace -}}
+    {{- tpl .Values.agent.namespace . -}}
+  {{- else -}}
+    {{- if .Values.namespaceOverride -}}
+      {{- .Values.namespaceOverride -}}
+    {{- else -}}
+      {{- .Release.Namespace -}}
+    {{- end -}}
+  {{- end -}}
+{{- end -}}
+
+
+{{/*
+Create a default fully qualified app name.
+We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
+If release name contains chart name it will be used as a full name.
+*/}}
+{{- define "jenkins.fullname" -}}
+{{- if .Values.fullnameOverride -}}
+{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
+{{- else -}}
+{{- $name := default .Chart.Name .Values.nameOverride -}}
+{{- if contains $name .Release.Name -}}
+{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
+{{- else -}}
+{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
+{{- end -}}
+{{- end -}}
+
+{{/*
+Returns the admin password
+https://github.com/helm/charts/issues/5167#issuecomment-619137759
+*/}}
+{{- define "jenkins.password" -}}
+  {{- if .Values.controller.admin.password -}}
+    {{- .Values.controller.admin.password | b64enc | quote }}
+  {{- else -}}
+    {{- $secret := (lookup "v1" "Secret" .Release.Namespace (include "jenkins.fullname" .)).data -}}
+    {{- if $secret -}}
+      {{/*
+        Reusing current password since secret exists
+      */}}
+      {{- index $secret ( .Values.controller.admin.passwordKey | default "jenkins-admin-password" ) -}}
+    {{- else -}}
+      {{/*
+          Generate new password
+      */}}
+      {{- randAlphaNum 22 | b64enc | quote }}
+    {{- end -}}
+  {{- end -}}
+{{- end -}}
+
+{{/*
+Returns the Jenkins URL
+*/}}
+{{- define "jenkins.url" -}}
+{{- if .Values.controller.jenkinsUrl }}
+  {{- .Values.controller.jenkinsUrl }}
+{{- else }}
+  {{- if .Values.controller.ingress.hostName }}
+    {{- if .Values.controller.ingress.tls }}
+      {{- default "https" .Values.controller.jenkinsUrlProtocol }}://{{ tpl .Values.controller.ingress.hostName $ }}{{ default "" .Values.controller.jenkinsUriPrefix }}
+    {{- else }}
+      {{- default "http" .Values.controller.jenkinsUrlProtocol }}://{{ tpl .Values.controller.ingress.hostName $ }}{{ default "" .Values.controller.jenkinsUriPrefix }}
+    {{- end }}
+  {{- else }}
+      {{- default "http" .Values.controller.jenkinsUrlProtocol }}://{{ template "jenkins.fullname" . }}:{{.Values.controller.servicePort}}{{ default "" .Values.controller.jenkinsUriPrefix }}
+  {{- end}}
+{{- end}}
+{{- end -}}
+
+{{/*
+Returns configuration as code default config
+*/}}
+{{- define "jenkins.casc.defaults" -}}
+jenkins:
+  {{- $configScripts := toYaml .Values.controller.JCasC.configScripts }}
+  {{- if and (.Values.controller.JCasC.authorizationStrategy) (not (contains "authorizationStrategy:" $configScripts)) }}
+  authorizationStrategy:
+    {{- tpl .Values.controller.JCasC.authorizationStrategy . | nindent 4 }}
+  {{- end }}
+  {{- if and (.Values.controller.JCasC.securityRealm) (not (contains "securityRealm:" $configScripts)) }}
+  securityRealm:
+    {{- tpl .Values.controller.JCasC.securityRealm . | nindent 4 }}
+  {{- end }}
+  disableRememberMe: {{ .Values.controller.disableRememberMe }}
+  {{- if .Values.controller.legacyRemotingSecurityEnabled }}
+  remotingSecurity:
+    enabled: true
+  {{- end }}
+  mode: {{ .Values.controller.executorMode }}
+  numExecutors: {{ .Values.controller.numExecutors }}
+  {{- if not (kindIs "invalid" .Values.controller.customJenkinsLabels) }}
+  labelString: "{{ join " " .Values.controller.customJenkinsLabels }}"
+  {{- end }}
+  {{- if .Values.controller.projectNamingStrategy }}
+  {{- if kindIs "string" .Values.controller.projectNamingStrategy }}
+  projectNamingStrategy: "{{ .Values.controller.projectNamingStrategy }}"
+  {{- else }}
+  projectNamingStrategy:
+    {{- toYaml .Values.controller.projectNamingStrategy | nindent 4 }}
+  {{- end }}
+  {{- end }}
+  markupFormatter:
+    {{- if .Values.controller.enableRawHtmlMarkupFormatter }}
+    rawHtml:
+      disableSyntaxHighlighting: true
+    {{- else }}
+    {{- toYaml .Values.controller.markupFormatter | nindent 4 }}
+    {{- end }}
+  clouds:
+  - kubernetes:
+      containerCapStr: "{{ .Values.agent.containerCap }}"
+      {{- if .Values.agent.jnlpregistry }}
+      jnlpregistry: "{{ .Values.agent.jnlpregistry }}"
+      {{- end }}
+      defaultsProviderTemplate: "{{ .Values.agent.defaultsProviderTemplate }}"
+      connectTimeout: "{{ .Values.agent.kubernetesConnectTimeout }}"
+      readTimeout: "{{ .Values.agent.kubernetesReadTimeout }}"
+      {{- if .Values.agent.directConnection }}
+      directConnection: true
+      {{- else }}
+      {{- if .Values.agent.jenkinsUrl }}
+      jenkinsUrl: "{{ tpl .Values.agent.jenkinsUrl . }}"
+      {{- else }}
+      jenkinsUrl: "http://{{ template "jenkins.fullname" . }}.{{ template "jenkins.namespace" . }}.svc.{{.Values.clusterZone}}:{{.Values.controller.servicePort}}{{ default "" .Values.controller.jenkinsUriPrefix }}"
+      {{- end }}
+      {{- if not .Values.agent.websocket }}
+      {{- if .Values.agent.jenkinsTunnel }}
+      jenkinsTunnel: "{{ tpl .Values.agent.jenkinsTunnel . }}"
+      {{- else }}
+      jenkinsTunnel: "{{ template "jenkins.fullname" . }}-agent.{{ template "jenkins.namespace" . }}.svc.{{.Values.clusterZone}}:{{ .Values.controller.agentListenerPort }}"
+      {{- end }}
+      {{- else }}
+      webSocket: true
+      {{- end }}
+      {{- end }}
+      maxRequestsPerHostStr: {{ .Values.agent.maxRequestsPerHostStr | quote }}
+      retentionTimeout: {{ .Values.agent.retentionTimeout | quote }}
+      waitForPodSec: {{ .Values.agent.waitForPodSec | quote }}
+      name: "{{ .Values.controller.cloudName }}"
+      namespace: "{{ template "jenkins.agent.namespace" . }}"
+      restrictedPssSecurityContext: {{ .Values.agent.restrictedPssSecurityContext }}
+      serverUrl: "{{ .Values.kubernetesURL }}"
+      credentialsId: "{{ .Values.credentialsId }}"
+      {{- if .Values.agent.enabled }}
+      podLabels:
+      - key: "jenkins/{{ .Release.Name }}-{{ .Values.agent.componentName }}"
+        value: "true"
+      {{- range $key, $val := .Values.agent.podLabels }}
+      - key: {{ $key | quote }}
+        value: {{ $val | quote }}
+      {{- end }}
+      templates:
+      {{- if not .Values.agent.disableDefaultAgent }}
+      {{- include "jenkins.casc.podTemplate" . | nindent 8 }}
+      {{- end }}
+      {{- if .Values.additionalAgents }}
+      {{- /* save .Values.agent */}}
+      {{- $agent := .Values.agent }}
+      {{- range $name, $additionalAgent := .Values.additionalAgents }}
+        {{- $additionalContainersEmpty := and (hasKey $additionalAgent "additionalContainers") (empty $additionalAgent.additionalContainers)  }}
+        {{- /* merge original .Values.agent into additional agent to ensure it at least has the default values */}}
+        {{- $additionalAgent := merge $additionalAgent $agent }}
+        {{- /* clear list of additional containers in case it is configured empty for this agent (merge might have overwritten that) */}}
+        {{- if $additionalContainersEmpty }}
+        {{- $_ := set $additionalAgent "additionalContainers" list }}
+        {{- end }}
+        {{- /* set .Values.agent to $additionalAgent */}}
+        {{- $_ := set $.Values "agent" $additionalAgent }}
+        {{- include "jenkins.casc.podTemplate" $ | nindent 8 }}
+      {{- end }}
+      {{- /* restore .Values.agent */}}
+      {{- $_ := set .Values "agent" $agent }}
+      {{- end }}
+      {{- if .Values.agent.podTemplates }}
+      {{- range $key, $val := .Values.agent.podTemplates }}
+        {{- tpl $val $ | nindent 8 }}
+      {{- end }}
+      {{- end }}
+      {{- end }}
+    {{- if .Values.additionalClouds }}
+    {{- /* save root */}}
+    {{- $oldRoot := deepCopy $ }}
+      {{- range $name, $additionalCloud := .Values.additionalClouds }}
+      {{- $newRoot := deepCopy $ }}
+      {{- /* clear additionalAgents from the copy if override set to `true` */}}
+      {{- if .additionalAgentsOverride }}
+      {{- $_ := set $newRoot.Values "additionalAgents" list}}
+      {{- end}}
+      {{- $newValues := merge $additionalCloud $newRoot.Values }}
+      {{- $_ := set $newRoot "Values" $newValues }}
+      {{- /* clear additionalClouds from the copy */}}
+      {{- $_ := set $newRoot.Values "additionalClouds" list }}
+      {{- with $newRoot}}
+  - kubernetes:
+      containerCapStr: "{{ .Values.agent.containerCap }}"
+      {{- if .Values.agent.jnlpregistry }}
+      jnlpregistry: "{{ .Values.agent.jnlpregistry }}"
+      {{- end }}
+      defaultsProviderTemplate: "{{ .Values.agent.defaultsProviderTemplate }}"
+      connectTimeout: "{{ .Values.agent.kubernetesConnectTimeout }}"
+      readTimeout: "{{ .Values.agent.kubernetesReadTimeout }}"
+      {{- if .Values.agent.directConnection }}
+      directConnection: true
+      {{- else }}
+      {{- if .Values.agent.jenkinsUrl }}
+      jenkinsUrl: "{{ tpl .Values.agent.jenkinsUrl . }}"
+      {{- else }}
+      jenkinsUrl: "http://{{ template "jenkins.fullname" . }}.{{ template "jenkins.namespace" . }}.svc.{{.Values.clusterZone}}:{{.Values.controller.servicePort}}{{ default "" .Values.controller.jenkinsUriPrefix }}"
+      {{- end }}
+      {{- if not .Values.agent.websocket }}
+      {{- if .Values.agent.jenkinsTunnel }}
+      jenkinsTunnel: "{{ tpl .Values.agent.jenkinsTunnel . }}"
+      {{- else }}
+      jenkinsTunnel: "{{ template "jenkins.fullname" . }}-agent.{{ template "jenkins.namespace" . }}.svc.{{.Values.clusterZone}}:{{ .Values.controller.agentListenerPort }}"
+      {{- end }}
+      {{- else }}
+      webSocket: true
+      {{- end }}
+      {{- end }}
+      maxRequestsPerHostStr: {{ .Values.agent.maxRequestsPerHostStr | quote }}
+      retentionTimeout: {{ .Values.agent.retentionTimeout | quote }}
+      waitForPodSec: {{ .Values.agent.waitForPodSec | quote }}
+      name: {{ $name | quote }}
+      namespace: "{{ template "jenkins.agent.namespace" . }}"
+      restrictedPssSecurityContext: {{ .Values.agent.restrictedPssSecurityContext }}
+      serverUrl: "{{ .Values.kubernetesURL }}"
+      credentialsId: "{{ .Values.credentialsId }}"
+      {{- if .Values.agent.enabled }}
+      podLabels:
+      - key: "jenkins/{{ .Release.Name }}-{{ .Values.agent.componentName }}"
+        value: "true"
+      {{- range $key, $val := .Values.agent.podLabels }}
+      - key: {{ $key | quote }}
+        value: {{ $val | quote }}
+      {{- end }}
+      templates:
+     {{- if not .Values.agent.disableDefaultAgent }}
+       {{- include "jenkins.casc.podTemplate" . | nindent 8 }}
+     {{- end }}
+     {{- if .Values.additionalAgents }}
+       {{- /* save .Values.agent */}}
+       {{- $agent := .Values.agent }}
+       {{- range $name, $additionalAgent := .Values.additionalAgents }}
+         {{- $additionalContainersEmpty := and (hasKey $additionalAgent "additionalContainers") (empty $additionalAgent.additionalContainers)  }}
+         {{- /* merge original .Values.agent into additional agent to ensure it at least has the default values */}}
+         {{- $additionalAgent := merge $additionalAgent $agent }}
+         {{- /* clear list of additional containers in case it is configured empty for this agent (merge might have overwritten that) */}}
+         {{- if $additionalContainersEmpty }}
+         {{- $_ := set $additionalAgent "additionalContainers" list }}
+         {{- end }}
+         {{- /* set .Values.agent to $additionalAgent */}}
+         {{- $_ := set $.Values "agent" $additionalAgent }}
+         {{- include "jenkins.casc.podTemplate" $ | nindent 8 }}
+       {{- end }}
+       {{- /* restore .Values.agent */}}
+       {{- $_ := set .Values "agent" $agent }}
+     {{- end }}
+       {{- with .Values.agent.podTemplates }}
+         {{- range $key, $val := . }}
+           {{- tpl $val $ | nindent 8 }}
+         {{- end }}
+       {{- end }}
+       {{- end }}
+     {{- end }}
+     {{- end }}
+  {{- /* restore root */}}
+  {{- $_ := set $ "Values" $oldRoot.Values }}
+  {{- end }}
+  {{- if .Values.controller.csrf.defaultCrumbIssuer.enabled }}
+  crumbIssuer:
+    standard:
+      excludeClientIPFromCrumb: {{ if .Values.controller.csrf.defaultCrumbIssuer.proxyCompatability }}true{{ else }}false{{- end }}
+  {{- end }}
+{{- include "jenkins.casc.security" . }}
+{{- with .Values.controller.scriptApproval }}
+  scriptApproval:
+    approvedSignatures:
+    {{- range $key, $val := . }}
+    - "{{ $val }}"
+    {{- end }}
+{{- end }}
+unclassified:
+  location:
+    {{- with .Values.controller.jenkinsAdminEmail }}
+    adminAddress: {{ . }}
+    {{- end }}
+    url: {{ template "jenkins.url" . }}
+{{- end -}}
+
+{{/*
+Returns a name template to be used for jcasc configmaps, using
+suffix passed in at call as index 0
+*/}}
+{{- define "jenkins.casc.configName" -}}
+{{- $name := index . 0 -}}
+{{- $root := index . 1 -}}
+"{{- include "jenkins.fullname" $root -}}-jenkins-{{ $name }}"
+{{- end -}}
+
+{{/*
+Returns kubernetes pod template configuration as code
+*/}}
+{{- define "jenkins.casc.podTemplate" -}}
+- name: "{{ .Values.agent.podName }}"
+  namespace: "{{ template "jenkins.agent.namespace" . }}"
+{{- if .Values.agent.annotations }}
+  annotations:
+  {{- range $key, $value := .Values.agent.annotations }}
+  - key: {{ $key }}
+    value: {{ $value | quote }}
+  {{- end }}
+{{- end }}
+  id: {{ sha256sum (toYaml .Values.agent) }}
+  containers:
+  - name: "{{ .Values.agent.sideContainerName }}"
+    alwaysPullImage: {{ .Values.agent.alwaysPullImage }}
+    args: "{{ .Values.agent.args | replace "$" "^$" }}"
+    {{- with .Values.agent.command }}
+    command: {{ . }}
+    {{- end }}
+    envVars:
+      - envVar:
+        {{- if .Values.agent.directConnection }}
+          key: "JENKINS_DIRECT_CONNECTION"
+          {{- if .Values.agent.jenkinsTunnel }}
+          value: "{{ tpl .Values.agent.jenkinsTunnel . }}"
+          {{- else }}
+          value: "{{ template "jenkins.fullname" . }}-agent.{{ template "jenkins.namespace" . }}.svc.{{.Values.clusterZone}}:{{ .Values.controller.agentListenerPort }}"
+          {{- end }}
+        {{- else }}
+          key: "JENKINS_URL"
+          {{- if .Values.agent.jenkinsUrl }}
+          value: {{ tpl .Values.agent.jenkinsUrl . }}
+          {{- else }}
+          value: "http://{{ template "jenkins.fullname" . }}.{{ template "jenkins.namespace" . }}.svc.{{.Values.clusterZone}}:{{.Values.controller.servicePort}}{{ default "/" .Values.controller.jenkinsUriPrefix }}"
+          {{- end }}
+        {{- end }}
+    image: "{{ .Values.agent.image.repository }}:{{ .Values.agent.image.tag }}"
+    {{- if .Values.agent.livenessProbe }}
+    livenessProbe:
+      execArgs: {{.Values.agent.livenessProbe.execArgs | quote}}
+      failureThreshold: {{.Values.agent.livenessProbe.failureThreshold}}
+      initialDelaySeconds: {{.Values.agent.livenessProbe.initialDelaySeconds}}
+      periodSeconds: {{.Values.agent.livenessProbe.periodSeconds}}
+      successThreshold: {{.Values.agent.livenessProbe.successThreshold}}
+      timeoutSeconds: {{.Values.agent.livenessProbe.timeoutSeconds}}
+    {{- end }}
+    privileged: "{{- if .Values.agent.privileged }}true{{- else }}false{{- end }}"
+    resourceLimitCpu: {{.Values.agent.resources.limits.cpu}}
+    resourceLimitMemory: {{.Values.agent.resources.limits.memory}}
+    {{- with .Values.agent.resources.limits.ephemeralStorage }}
+    resourceLimitEphemeralStorage: {{.}}
+    {{- end }}
+    resourceRequestCpu: {{.Values.agent.resources.requests.cpu}}
+    resourceRequestMemory: {{.Values.agent.resources.requests.memory}}
+    {{- with .Values.agent.resources.requests.ephemeralStorage }}
+    resourceRequestEphemeralStorage: {{.}}
+    {{- end }}
+    {{- with .Values.agent.runAsUser }}
+    runAsUser: {{ . }}
+    {{- end }}
+    {{- with .Values.agent.runAsGroup }}
+    runAsGroup: {{ . }}
+    {{- end }}
+    ttyEnabled: {{ .Values.agent.TTYEnabled }}
+    workingDir: {{ .Values.agent.workingDir }}
+{{- range $additionalContainers := .Values.agent.additionalContainers }}
+  - name: "{{ $additionalContainers.sideContainerName }}"
+    alwaysPullImage: {{ $additionalContainers.alwaysPullImage | default $.Values.agent.alwaysPullImage }}
+    args: "{{ $additionalContainers.args | replace "$" "^$" }}"
+    {{- with $additionalContainers.command }}
+    command: {{ . }}
+    {{- end }}
+    envVars:
+      - envVar:
+          key: "JENKINS_URL"
+          {{- if $additionalContainers.jenkinsUrl }}
+          value: {{ tpl ($additionalContainers.jenkinsUrl) . }}
+          {{- else }}
+          value: "http://{{ template "jenkins.fullname" $ }}.{{ template "jenkins.namespace" $ }}.svc.{{ $.Values.clusterZone }}:{{ $.Values.controller.servicePort }}{{ default "/" $.Values.controller.jenkinsUriPrefix }}"
+          {{- end }}
+    image: "{{ $additionalContainers.image.repository }}:{{ $additionalContainers.image.tag }}"
+    {{- if $additionalContainers.livenessProbe }}
+    livenessProbe:
+      execArgs: {{$additionalContainers.livenessProbe.execArgs | quote}}
+      failureThreshold: {{$additionalContainers.livenessProbe.failureThreshold}}
+      initialDelaySeconds: {{$additionalContainers.livenessProbe.initialDelaySeconds}}
+      periodSeconds: {{$additionalContainers.livenessProbe.periodSeconds}}
+      successThreshold: {{$additionalContainers.livenessProbe.successThreshold}}
+      timeoutSeconds: {{$additionalContainers.livenessProbe.timeoutSeconds}}
+    {{- end }}
+    privileged: "{{- if $additionalContainers.privileged }}true{{- else }}false{{- end }}"
+    resourceLimitCpu: {{ if $additionalContainers.resources }}{{ $additionalContainers.resources.limits.cpu }}{{ else }}{{ $.Values.agent.resources.limits.cpu }}{{ end }}
+    resourceLimitMemory: {{ if $additionalContainers.resources }}{{ $additionalContainers.resources.limits.memory }}{{ else }}{{ $.Values.agent.resources.limits.memory }}{{ end }}
+    resourceRequestCpu: {{ if $additionalContainers.resources }}{{ $additionalContainers.resources.requests.cpu }}{{ else }}{{ $.Values.agent.resources.requests.cpu }}{{ end }}
+    resourceRequestMemory: {{ if $additionalContainers.resources }}{{ $additionalContainers.resources.requests.memory }}{{ else }}{{ $.Values.agent.resources.requests.memory }}{{ end }}
+    {{- if or $additionalContainers.runAsUser $.Values.agent.runAsUser }}
+    runAsUser: {{ $additionalContainers.runAsUser | default $.Values.agent.runAsUser }}
+    {{- end }}
+    {{- if or $additionalContainers.runAsGroup $.Values.agent.runAsGroup }}
+    runAsGroup: {{ $additionalContainers.runAsGroup | default $.Values.agent.runAsGroup }}
+    {{- end }}
+    ttyEnabled: {{ $additionalContainers.TTYEnabled | default $.Values.agent.TTYEnabled }}
+    workingDir: {{ $additionalContainers.workingDir | default $.Values.agent.workingDir }}
+{{- end }}
+{{- if or .Values.agent.envVars .Values.agent.secretEnvVars }}
+  envVars:
+  {{- range $index, $var := .Values.agent.envVars }}
+    - envVar:
+        key: {{ $var.name }}
+        value: {{ tpl $var.value $ }}
+  {{- end }}
+  {{- range $index, $var := .Values.agent.secretEnvVars }}
+    - secretEnvVar:
+        key: {{ $var.key }}
+        secretName: {{ $var.secretName }}
+        secretKey: {{ $var.secretKey }}
+        optional: {{ $var.optional | default false }}
+  {{- end }}
+{{- end }}
+  idleMinutes: {{ .Values.agent.idleMinutes }}
+  instanceCap: 2147483647
+  {{- if .Values.agent.hostNetworking }}
+  hostNetwork: {{ .Values.agent.hostNetworking }}
+  {{- end }}
+  {{- if .Values.agent.imagePullSecretName }}
+  imagePullSecrets:
+  - name: {{ .Values.agent.imagePullSecretName }}
+  {{- end }}
+  label: "{{ .Release.Name }}-{{ .Values.agent.componentName }} {{ .Values.agent.customJenkinsLabels  | join " " }}"
+{{- if .Values.agent.nodeSelector }}
+  nodeSelector:
+  {{- $local := dict "first" true }}
+  {{- range $key, $value := .Values.agent.nodeSelector }}
+    {{- if $local.first }} {{ else }},{{ end }}
+    {{- $key }}={{ tpl $value $ }}
+    {{- $_ := set $local "first" false }}
+  {{- end }}
+{{- end }}
+  nodeUsageMode: {{ quote .Values.agent.nodeUsageMode }}
+  podRetention: {{ .Values.agent.podRetention }}
+  showRawYaml: {{ .Values.agent.showRawYaml }}
+  serviceAccount: "{{ include "jenkins.serviceAccountAgentName" . }}"
+  slaveConnectTimeoutStr: "{{ .Values.agent.connectTimeout }}"
+{{- if .Values.agent.volumes }}
+  volumes:
+  {{- range $index, $volume := .Values.agent.volumes }}
+    -{{- if (eq $volume.type "ConfigMap") }} configMapVolume:
+     {{- else if (eq $volume.type "EmptyDir") }} emptyDirVolume:
+     {{- else if (eq $volume.type "EphemeralVolume") }} genericEphemeralVolume:
+     {{- else if (eq $volume.type "HostPath") }} hostPathVolume:
+     {{- else if (eq $volume.type "Nfs") }} nfsVolume:
+     {{- else if (eq $volume.type "PVC") }} persistentVolumeClaim:
+     {{- else if (eq $volume.type "Secret") }} secretVolume:
+     {{- else }} {{ $volume.type }}:
+     {{- end }}
+    {{- range $key, $value := $volume }}
+      {{- if not (eq $key "type") }}
+        {{ $key }}: {{ if kindIs "string" $value }}{{ tpl $value $ | quote }}{{ else }}{{ $value }}{{ end }}
+      {{- end }}
+    {{- end }}
+  {{- end }}
+{{- end }}
+{{- if .Values.agent.workspaceVolume }}
+  workspaceVolume:
+    {{- if (eq .Values.agent.workspaceVolume.type "DynamicPVC") }}
+    dynamicPVC:
+    {{- else if (eq .Values.agent.workspaceVolume.type "EmptyDir") }}
+    emptyDirWorkspaceVolume:
+    {{- else if (eq .Values.agent.workspaceVolume.type "EphemeralVolume") }}
+    genericEphemeralVolume:
+    {{- else if (eq .Values.agent.workspaceVolume.type "HostPath") }}
+    hostPathWorkspaceVolume:
+    {{- else if (eq .Values.agent.workspaceVolume.type "Nfs") }}
+    nfsWorkspaceVolume:
+    {{- else if (eq .Values.agent.workspaceVolume.type "PVC") }}
+    persistentVolumeClaimWorkspaceVolume:
+    {{- else }}
+    {{ .Values.agent.workspaceVolume.type }}:
+    {{- end }}
+  {{- range $key, $value := .Values.agent.workspaceVolume }}
+    {{- if not (eq $key "type") }}
+      {{ $key }}: {{ if kindIs "string" $value }}{{ tpl $value $ | quote }}{{ else }}{{ $value }}{{ end }}
+    {{- end }}
+  {{- end }}
+{{- end }}
+{{- if .Values.agent.yamlTemplate }}
+  yaml: |-
+    {{- tpl (trim .Values.agent.yamlTemplate) . | nindent 4 }}
+{{- end }}
+  yamlMergeStrategy: {{ .Values.agent.yamlMergeStrategy }}
+{{- end -}}
+
+{{- define "jenkins.kubernetes-version" -}}
+  {{- if .Values.controller.installPlugins -}}
+    {{- range .Values.controller.installPlugins -}}
+      {{- if hasPrefix "kubernetes:" . }}
+        {{- $split := splitList ":" . }}
+        {{- printf "%s" (index $split 1 ) -}}
+      {{- end -}}
+    {{- end -}}
+  {{- end -}}
+{{- end -}}
+
+{{- define "jenkins.casc.security" }}
+security:
+{{- with .Values.controller.JCasC }}
+{{- if .security }}
+  {{- .security | toYaml | nindent 2 }}
+{{- end }}
+{{- end }}
+{{- end -}}
+
+{{/*
+Create the name of the service account to use
+*/}}
+{{- define "jenkins.serviceAccountName" -}}
+{{- if .Values.serviceAccount.create -}}
+    {{ default (include "jenkins.fullname" .) .Values.serviceAccount.name }}
+{{- else -}}
+    {{ default "default" .Values.serviceAccount.name }}
+{{- end -}}
+{{- end -}}
+
+{{/*
+Create the name of the service account for Jenkins agents to use
+*/}}
+{{- define "jenkins.serviceAccountAgentName" -}}
+{{- if .Values.serviceAccountAgent.create -}}
+    {{ default (printf "%s-%s" (include "jenkins.fullname" .) "agent") .Values.serviceAccountAgent.name }}
+{{- else -}}
+    {{ default "default" .Values.serviceAccountAgent.name }}
+{{- end -}}
+{{- end -}}
+
+{{/*
+Create a full tag name for controller image
+*/}}
+{{- define "controller.image.tag" -}}
+{{- if .Values.controller.image.tagLabel -}}
+    {{- default (printf "%s-%s" .Chart.AppVersion .Values.controller.image.tagLabel) .Values.controller.image.tag -}}
+{{- else -}}
+    {{- default .Chart.AppVersion .Values.controller.image.tag -}}
+{{- end -}}
+{{- end -}}
+
+{{/*
+Create the HTTP port for interacting with the controller
+*/}}
+{{- define "controller.httpPort" -}}
+{{- if .Values.controller.httpsKeyStore.enable -}}
+    {{- .Values.controller.httpsKeyStore.httpPort -}}
+{{- else -}}
+    {{- .Values.controller.targetPort -}}
+{{- end -}}
+{{- end -}}
+
+{{- define "jenkins.configReloadContainer" -}}
+{{- $root := index . 0 -}}
+{{- $containerName := index . 1 -}}
+{{- $containerType := index . 2 -}}
+- name: {{ $containerName }}
+  image: "{{ $root.Values.controller.sidecars.configAutoReload.image.registry }}/{{ $root.Values.controller.sidecars.configAutoReload.image.repository }}:{{ $root.Values.controller.sidecars.configAutoReload.image.tag }}"
+  imagePullPolicy: {{ $root.Values.controller.sidecars.configAutoReload.imagePullPolicy }}
+  {{- if $root.Values.controller.sidecars.configAutoReload.containerSecurityContext }}
+  securityContext: {{- toYaml $root.Values.controller.sidecars.configAutoReload.containerSecurityContext | nindent 4 }}
+  {{- end }}
+  {{- if $root.Values.controller.sidecars.configAutoReload.envFrom }}
+  envFrom:
+{{ (tpl (toYaml $root.Values.controller.sidecars.configAutoReload.envFrom) $root) | indent 4 }}
+  {{- end }}
+  env:
+    - name: POD_NAME
+      valueFrom:
+        fieldRef:
+          fieldPath: metadata.name
+    - name: LABEL
+      value: "{{ template "jenkins.fullname" $root }}-jenkins-config"
+    - name: FOLDER
+      value: "{{ $root.Values.controller.sidecars.configAutoReload.folder }}"
+    - name: NAMESPACE
+      value: '{{ $root.Values.controller.sidecars.configAutoReload.searchNamespace | default (include "jenkins.namespace" $root) }}'
+    {{- if eq $containerType "init" }}
+    - name: METHOD
+      value: "LIST"
+    {{- else if $root.Values.controller.sidecars.configAutoReload.sleepTime }}
+    - name: METHOD
+      value: "SLEEP"
+    - name: SLEEP_TIME
+      value: "{{ $root.Values.controller.sidecars.configAutoReload.sleepTime }}"
+    {{- end }}
+    {{- if eq $containerType "sidecar" }}
+    - name: REQ_URL
+      value: "{{- default "http" $root.Values.controller.sidecars.configAutoReload.scheme }}://localhost:{{- include "controller.httpPort" $root -}}{{- $root.Values.controller.jenkinsUriPrefix -}}/reload-configuration-as-code/?casc-reload-token=$(POD_NAME)"
+    - name: REQ_METHOD
+      value: "POST"
+    - name: REQ_RETRY_CONNECT
+      value: "{{ $root.Values.controller.sidecars.configAutoReload.reqRetryConnect }}"
+      {{- if $root.Values.controller.sidecars.configAutoReload.skipTlsVerify }}
+    - name: REQ_SKIP_TLS_VERIFY
+      value: "true"
+      {{- end }}
+    {{- end }}
+
+    {{- if $root.Values.controller.sidecars.configAutoReload.env }}
+    {{- range $envVarItem := $root.Values.controller.sidecars.configAutoReload.env -}}
+        {{- if or (ne $containerType "init") (ne .name "METHOD") }}
+{{- (tpl (toYaml (list $envVarItem)) $root) | nindent 4 }}
+        {{- end -}}
+    {{- end -}}
+    {{- end }}
+
+  resources:
+{{ toYaml $root.Values.controller.sidecars.configAutoReload.resources | indent 4 }}
+  volumeMounts:
+    - name: sc-config-volume
+      mountPath: {{ $root.Values.controller.sidecars.configAutoReload.folder | quote }}
+    - name: jenkins-home
+      mountPath: {{ $root.Values.controller.jenkinsHome }}
+      {{- if $root.Values.persistence.subPath }}
+      subPath: {{ $root.Values.persistence.subPath }}
+      {{- end }}
+
+{{- end -}}
diff --git a/charts/jenkins/templates/config-init-scripts.yaml b/charts/jenkins/templates/config-init-scripts.yaml
new file mode 100644
index 0000000..7dd253c
--- /dev/null
+++ b/charts/jenkins/templates/config-init-scripts.yaml
@@ -0,0 +1,18 @@
+{{- if .Values.controller.initScripts -}}
+
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: {{ template "jenkins.fullname" . }}-init-scripts
+  namespace: {{ template "jenkins.namespace" . }}
+  labels:
+    "app.kubernetes.io/name": '{{ template "jenkins.name" .}}'
+    "app.kubernetes.io/managed-by": "{{ .Release.Service }}"
+    "app.kubernetes.io/instance": "{{ .Release.Name }}"
+    "app.kubernetes.io/component": "{{ .Values.controller.componentName }}"
+data:
+{{- range $key, $val := .Values.controller.initScripts }}
+  init{{ $key }}.groovy: |-
+{{ tpl $val $ | indent 4 }}
+{{- end }}
+{{- end }}
diff --git a/charts/jenkins/templates/config.yaml b/charts/jenkins/templates/config.yaml
new file mode 100644
index 0000000..5de0b9f
--- /dev/null
+++ b/charts/jenkins/templates/config.yaml
@@ -0,0 +1,92 @@
+{{- $jenkinsHome := .Values.controller.jenkinsHome -}}
+
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: {{ template "jenkins.fullname" . }}
+  namespace: {{ template "jenkins.namespace" . }}
+  labels:
+    "app.kubernetes.io/name": '{{ template "jenkins.name" .}}'
+    "app.kubernetes.io/managed-by": "{{ .Release.Service }}"
+    "app.kubernetes.io/instance": "{{ .Release.Name }}"
+    "app.kubernetes.io/component": "{{ .Values.controller.componentName }}"
+data:
+  apply_config.sh: |-
+    set -e
+{{- if .Values.controller.initializeOnce }}
+    if [ -f {{ .Values.controller.jenkinsHome }}/initialization-completed ]; then
+      echo "controller was previously initialized, refusing to re-initialize"
+      exit 0
+    fi
+{{- end }}
+    echo "disable Setup Wizard"
+    # Prevent Setup Wizard when JCasC is enabled
+    echo $JENKINS_VERSION > {{ .Values.controller.jenkinsHome }}/jenkins.install.UpgradeWizard.state
+    echo $JENKINS_VERSION > {{ .Values.controller.jenkinsHome }}/jenkins.install.InstallUtil.lastExecVersion
+{{- if .Values.controller.overwritePlugins }}
+    echo "remove all plugins from shared volume"
+    # remove all plugins from shared volume
+    rm -rf {{ .Values.controller.jenkinsHome }}/plugins/*
+{{- end }}
+{{- if .Values.controller.JCasC.overwriteConfiguration }}
+    echo "deleting all XML config files"
+    rm -f {{ .Values.controller.jenkinsHome }}/config.xml
+    rm -f {{ .Values.controller.jenkinsHome }}/*plugins*.xml
+    find {{ .Values.controller.jenkinsHome }} -maxdepth 1 -type f -iname '*configuration*.xml' -exec rm -f {} \;
+{{- end }}
+{{- if .Values.controller.installPlugins }}
+    echo "download plugins"
+    # Install missing plugins
+    cp /var/jenkins_config/plugins.txt {{ .Values.controller.jenkinsHome }};
+    rm -rf {{ .Values.controller.jenkinsRef }}/plugins/*.lock
+    version () { echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'; }
+    if [ -f "{{ .Values.controller.jenkinsWar }}" ] && [ -n "$(command -v jenkins-plugin-cli)" 2>/dev/null ] && [ $(version $(jenkins-plugin-cli --version)) -ge $(version "2.1.1") ]; then
+      jenkins-plugin-cli --verbose --war "{{ .Values.controller.jenkinsWar }}" --plugin-file "{{ .Values.controller.jenkinsHome }}/plugins.txt" --latest {{ .Values.controller.installLatestPlugins }}{{- if .Values.controller.installLatestSpecifiedPlugins }} --latest-specified{{- end }};
+    else
+      /usr/local/bin/install-plugins.sh `echo $(cat {{ .Values.controller.jenkinsHome }}/plugins.txt)`;
+    fi
+    echo "copy plugins to shared volume"
+    # Copy plugins to shared volume
+    yes n | cp -i {{ .Values.controller.jenkinsRef }}/plugins/* /var/jenkins_plugins/;
+{{- end }}
+  {{- if not .Values.controller.sidecars.configAutoReload.enabled }}
+    echo "copy configuration as code files"
+    mkdir -p {{ .Values.controller.jenkinsHome }}/casc_configs;
+    rm -rf {{ .Values.controller.jenkinsHome }}/casc_configs/*
+    {{- if or .Values.controller.JCasC.defaultConfig .Values.controller.JCasC.configScripts }}
+    cp -v /var/jenkins_config/*.yaml {{ .Values.controller.jenkinsHome }}/casc_configs
+    {{- end }}
+  {{- end }}
+    echo "finished initialization"
+{{- if .Values.controller.initializeOnce }}
+    touch {{ .Values.controller.jenkinsHome }}/initialization-completed
+{{- end }}
+  {{- if not .Values.controller.sidecars.configAutoReload.enabled }}
+# Only add config to this script if we aren't auto-reloading otherwise the pod will restart upon each config change:
+{{- if .Values.controller.JCasC.defaultConfig }}
+  jcasc-default-config.yaml: |-
+    {{- include "jenkins.casc.defaults" . |nindent 4}}
+{{- end }}
+{{- range $key, $val := .Values.controller.JCasC.configScripts }}
+  {{ $key }}.yaml: |-
+{{ tpl $val $| indent 4 }}
+{{- end }}
+{{- end }}
+  plugins.txt: |-
+{{- if .Values.controller.installPlugins }}
+  {{- range $installPlugin := .Values.controller.installPlugins }}
+    {{- $installPlugin | nindent 4 }}
+  {{- end }}
+  {{- range $addlPlugin := .Values.controller.additionalPlugins }}
+    {{- /* duplicate plugin check */}}
+    {{- range $installPlugin := $.Values.controller.installPlugins }}
+      {{- if eq (splitList ":" $addlPlugin | first) (splitList ":" $installPlugin | first) }}
+        {{- $message := print "[PLUGIN CONFLICT] controller.additionalPlugins contains '" $addlPlugin "'" }}
+        {{- $message := print $message " but controller.installPlugins already contains '" $installPlugin "'." }}
+        {{- $message := print $message " Override controller.installPlugins to use '" $addlPlugin "' plugin." }}
+        {{- fail $message }}
+      {{- end }}
+    {{- end }}
+    {{- $addlPlugin | nindent 4 }}
+  {{- end }}
+{{- end }}
diff --git a/charts/jenkins/templates/deprecation.yaml b/charts/jenkins/templates/deprecation.yaml
new file mode 100644
index 0000000..f54017c
--- /dev/null
+++ b/charts/jenkins/templates/deprecation.yaml
@@ -0,0 +1,151 @@
+{{- if .Values.checkDeprecation }}
+   {{- if .Values.master }}
+      {{ fail "`master` does no longer exist. It has been renamed to `controller`" }}
+   {{- end }}
+
+   {{- if .Values.controller.imageTag }}
+      {{ fail "`controller.imageTag` does no longer exist. Please use `controller.image.tag` instead" }}
+   {{- end }}
+
+   {{- if .Values.controller.slaveListenerPort }}
+      {{ fail "`controller.slaveListenerPort` does no longer exist. It has been renamed to `controller.agentListenerPort`" }}
+   {{- end }}
+
+   {{- if .Values.controller.slaveHostPort }}
+      {{ fail "`controller.slaveHostPort` does no longer exist. It has been renamed to `controller.agentListenerHostPort`" }}
+   {{- end }}
+
+   {{- if .Values.controller.slaveKubernetesNamespace }}
+      {{ fail "`controller.slaveKubernetesNamespace` does no longer exist. It has been renamed to `agent.namespace`" }}
+   {{- end }}
+
+   {{- if .Values.controller.slaveDefaultsProviderTemplate }}
+      {{ fail "`controller.slaveDefaultsProviderTemplate` does no longer exist. It has been renamed to `agent.defaultsProviderTemplate`" }}
+   {{- end }}
+
+   {{- if .Values.controller.useSecurity }}
+      {{ fail "`controller.useSecurity` does no longer exist. It has been renamed to `controller.adminSecret`" }}
+   {{- end }}
+
+   {{- if .Values.controller.slaveJenkinsUrl }}
+      {{ fail "`controller.slaveJenkinsUrl` does no longer exist. It has been renamed to `agent.jenkinsUrl`" }}
+   {{- end }}
+
+   {{- if .Values.controller.slaveJenkinsTunnel }}
+      {{ fail "`controller.slaveJenkinsTunnel` does no longer exist. It has been renamed to `agent.jenkinsTunnel`" }}
+   {{- end }}
+
+   {{- if .Values.controller.slaveConnectTimeout }}
+      {{ fail "`controller.slaveConnectTimeout` does no longer exist. It has been renamed to `agent.kubernetesConnectTimeout`" }}
+   {{- end }}
+
+   {{- if .Values.controller.slaveReadTimeout }}
+      {{ fail "`controller.slaveReadTimeout` does no longer exist. It has been renamed to `agent.kubernetesReadTimeout`" }}
+   {{- end }}
+
+   {{- if .Values.controller.slaveListenerServiceType }}
+      {{ fail "`controller.slaveListenerServiceType` does no longer exist. It has been renamed to `controller.agentListenerServiceType`" }}
+   {{- end }}
+
+   {{- if .Values.controller.slaveListenerLoadBalancerIP }}
+      {{ fail "`controller.slaveListenerLoadBalancerIP` does no longer exist. It has been renamed to `controller.agentListenerLoadBalancerIP`" }}
+   {{- end }}
+
+   {{- if .Values.controller.slaveListenerServiceAnnotations }}
+      {{ fail "`controller.slaveListenerServiceAnnotations` does no longer exist. It has been renamed to `controller.agentListenerServiceAnnotations`" }}
+   {{- end }}
+
+   {{- if .Values.agent.slaveConnectTimeout }}
+      {{ fail "`agent.slaveConnectTimeout` does no longer exist. It has been renamed to `agent.connectTimeout`" }}
+   {{- end }}
+
+   {{- if .Values.NetworkPolicy }}
+
+      {{- if .Values.NetworkPolicy.Enabled }}
+         {{ fail "`NetworkPolicy.Enabled` does no longer exist. It has been renamed to `networkPolicy.enabled`" }}
+      {{- end }}
+
+      {{- if .Values.NetworkPolicy.ApiVersion }}
+         {{ fail "`NetworkPolicy.ApiVersion` does no longer exist. It has been renamed to `networkPolicy.apiVersion`" }}
+      {{- end }}
+
+      {{ fail "NetworkPolicy.* values have been renamed, please check the documentation" }}
+   {{- end }}
+
+
+   {{- if .Values.rbac.install }}
+      {{ fail "`rbac.install` does no longer exist. It has been renamed to `rbac.create` and is enabled by default!" }}
+   {{- end }}
+
+   {{- if .Values.rbac.serviceAccountName }}
+      {{ fail "`rbac.serviceAccountName` does no longer exist. It has been renamed to `serviceAccount.name`" }}
+   {{- end }}
+
+   {{- if .Values.rbac.serviceAccountAnnotations }}
+      {{ fail "`rbac.serviceAccountAnnotations` does no longer exist. It has been renamed to `serviceAccount.annotations`" }}
+   {{- end }}
+
+   {{- if .Values.rbac.roleRef }}
+      {{ fail "`rbac.roleRef` does no longer exist. RBAC roles are now generated, please check the documentation" }}
+   {{- end }}
+
+   {{- if .Values.rbac.roleKind }}
+      {{ fail "`rbac.roleKind` does no longer exist. RBAC roles are now generated, please check the documentation" }}
+   {{- end }}
+
+   {{- if .Values.rbac.roleBindingKind }}
+      {{ fail "`rbac.roleBindingKind` does no longer exist. RBAC roles are now generated, please check the documentation" }}
+   {{- end }}
+
+   {{- if .Values.controller.JCasC.pluginVersion }}
+      {{ fail "controller.JCasC.pluginVersion has been deprecated, please use controller.installPlugins instead" }}
+   {{- end }}
+
+   {{- if .Values.controller.deploymentLabels }}
+   {{ fail "`controller.deploymentLabels` does no longer exist. It has been renamed to `controller.statefulSetLabels`" }}
+   {{- end }}
+
+   {{- if .Values.controller.deploymentAnnotations }}
+   {{ fail "`controller.deploymentAnnotations` does no longer exist. It has been renamed to `controller.statefulSetAnnotations`" }}
+   {{- end }}
+
+   {{- if .Values.controller.rollingUpdate }}
+   {{ fail "`controller.rollingUpdate` does no longer exist. It is no longer relevant, since a StatefulSet is used for the Jenkins controller" }}
+   {{- end }}
+
+   {{- if .Values.controller.tag }}
+   {{ fail "`controller.tag` no longer exists. It has been renamed to `controller.image.tag'" }}
+   {{- end }}
+
+   {{- if .Values.controller.tagLabel }}
+   {{ fail "`controller.tagLabel` no longer exists. It has been renamed to `controller.image.tagLabel`" }}
+   {{- end }}
+
+   {{- if .Values.controller.adminSecret }}
+   {{ fail "`controller.adminSecret` no longer exists. It has been renamed to `controller.admin.createSecret`" }}
+   {{- end }}
+
+   {{- if .Values.controller.adminUser }}
+   {{ fail "`controller.adminUser` no longer exists. It has been renamed to `controller.admin.username`" }}
+   {{- end }}
+
+   {{- if .Values.controller.adminPassword }}
+   {{ fail "`controller.adminPassword` no longer exists. It has been renamed to `controller.admin.password`" }}
+   {{- end }}
+
+   {{- if .Values.controller.sidecars.other }}
+   {{ fail "`controller.sidecars.other` no longer exists. It has been renamed to `controller.sidecars.additionalSidecarContainers`" }}
+   {{- end }}
+
+   {{- if .Values.agent.tag }}
+   {{ fail "`controller.agent.tag` no longer exists. It has been renamed to `controller.agent.image.tag`" }}
+   {{- end }}
+
+   {{- if .Values.backup }}
+   {{ fail "`controller.backup` no longer exists." }}
+   {{- end }}
+
+   {{- if .Values.helmtest.bats.tag }}
+   {{ fail "`helmtest.bats.tag` no longer exists. It has been renamed to `helmtest.bats.image.tag`" }}
+   {{- end }}
+{{- end }}
diff --git a/charts/jenkins/templates/home-pvc.yaml b/charts/jenkins/templates/home-pvc.yaml
new file mode 100644
index 0000000..f417d23
--- /dev/null
+++ b/charts/jenkins/templates/home-pvc.yaml
@@ -0,0 +1,41 @@
+{{- if not (contains "jenkins-home" (quote .Values.persistence.volumes)) }}
+{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) -}}
+kind: PersistentVolumeClaim
+apiVersion: v1
+metadata:
+{{- if .Values.persistence.annotations }}
+  annotations:
+{{ toYaml .Values.persistence.annotations | indent 4 }}
+{{- end }}
+  name: {{ template "jenkins.fullname" . }}
+  namespace: {{ template "jenkins.namespace" . }}
+  labels:
+    "app.kubernetes.io/name": '{{ template "jenkins.name" .}}'
+    {{- if .Values.renderHelmLabels }}
+    "helm.sh/chart": "{{ template "jenkins.label" .}}"
+    {{- end }}
+    "app.kubernetes.io/managed-by": "{{ .Release.Service }}"
+    "app.kubernetes.io/instance": "{{ .Release.Name }}"
+    "app.kubernetes.io/component": "{{ .Values.controller.componentName }}"
+{{- if .Values.persistence.labels }}
+{{ toYaml .Values.persistence.labels | indent 4 }}
+{{- end }}
+spec:
+{{- if .Values.persistence.dataSource }}
+  dataSource:
+{{ toYaml .Values.persistence.dataSource | indent 4 }}
+{{- end }}
+  accessModes:
+    - {{ .Values.persistence.accessMode | quote }}
+  resources:
+    requests:
+      storage: {{ .Values.persistence.size | quote }}
+{{- if .Values.persistence.storageClass }}
+{{- if (eq "-" .Values.persistence.storageClass) }}
+  storageClassName: ""
+{{- else }}
+  storageClassName: "{{ .Values.persistence.storageClass }}"
+{{- end }}
+{{- end }}
+{{- end }}
+{{- end }}
diff --git a/charts/jenkins/templates/jcasc-config.yaml b/charts/jenkins/templates/jcasc-config.yaml
new file mode 100644
index 0000000..e404194
--- /dev/null
+++ b/charts/jenkins/templates/jcasc-config.yaml
@@ -0,0 +1,45 @@
+{{- $root := . }}
+{{- if .Values.controller.sidecars.configAutoReload.enabled }}
+{{- range $key, $val := .Values.controller.JCasC.configScripts }}
+{{- if $val }}
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: {{ template "jenkins.casc.configName" (list (printf "config-%s" $key) $ )}}
+  namespace: {{ template "jenkins.namespace" $root }}
+  labels:
+    "app.kubernetes.io/name": {{ template "jenkins.name" $root}}
+    {{- if $root.Values.renderHelmLabels }}
+    "helm.sh/chart": "{{ $root.Chart.Name }}-{{ $root.Chart.Version }}"
+    {{- end }}
+    "app.kubernetes.io/managed-by": "{{ $.Release.Service }}"
+    "app.kubernetes.io/instance": "{{ $.Release.Name }}"
+    "app.kubernetes.io/component": "{{ $.Values.controller.componentName }}"
+    {{ template "jenkins.fullname" $root }}-jenkins-config: "true"
+data:
+  {{ $key }}.yaml: |-
+{{ tpl $val $| indent 4 }}
+{{- end }}
+{{- end }}
+{{- if .Values.controller.JCasC.defaultConfig }}
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: {{ template "jenkins.casc.configName" (list "jcasc-config" $ )}}
+  namespace: {{ template "jenkins.namespace" $root }}
+  labels:
+    "app.kubernetes.io/name": {{ template "jenkins.name" $root}}
+    {{- if .Values.renderHelmLabels }}
+    "helm.sh/chart": "{{ $root.Chart.Name }}-{{ $root.Chart.Version }}"
+    {{- end }}
+    "app.kubernetes.io/managed-by": "{{ $.Release.Service }}"
+    "app.kubernetes.io/instance": "{{ $.Release.Name }}"
+    "app.kubernetes.io/component": "{{ $.Values.controller.componentName }}"
+    {{ template "jenkins.fullname" $root }}-jenkins-config: "true"
+data:
+  jcasc-default-config.yaml: |-
+    {{- include "jenkins.casc.defaults" . | nindent 4 }}
+{{- end}}
+{{- end }}
diff --git a/charts/jenkins/templates/jenkins-agent-svc.yaml b/charts/jenkins/templates/jenkins-agent-svc.yaml
new file mode 100644
index 0000000..4440b91
--- /dev/null
+++ b/charts/jenkins/templates/jenkins-agent-svc.yaml
@@ -0,0 +1,43 @@
+{{- if .Values.controller.agentListenerEnabled }}
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ template "jenkins.fullname" . }}-agent
+  namespace: {{ template "jenkins.namespace" . }}
+  labels:
+    "app.kubernetes.io/name": '{{ template "jenkins.name" .}}'
+    {{- if .Values.renderHelmLabels }}
+    "helm.sh/chart": "{{ template "jenkins.label" .}}"
+    {{- end }}
+    "app.kubernetes.io/managed-by": "{{ .Release.Service }}"
+    "app.kubernetes.io/instance": "{{ .Release.Name }}"
+    "app.kubernetes.io/component": "{{ .Values.controller.componentName }}"
+  {{- if .Values.controller.agentListenerServiceAnnotations }}
+  annotations:
+    {{- toYaml .Values.controller.agentListenerServiceAnnotations | nindent 4 }}
+  {{- end }}
+spec:
+  {{- if .Values.controller.agentListenerExternalTrafficPolicy }}
+  externalTrafficPolicy: {{.Values.controller.agentListenerExternalTrafficPolicy}}
+  {{- end }}
+  ports:
+    - port: {{ .Values.controller.agentListenerPort }}
+      targetPort: {{ .Values.controller.agentListenerPort }}
+      {{- if (and (eq .Values.controller.agentListenerServiceType "NodePort") (not (empty .Values.controller.agentListenerNodePort))) }}
+      nodePort: {{ .Values.controller.agentListenerNodePort }}
+      {{- end }}
+      name: agent-listener
+  selector:
+    "app.kubernetes.io/component": "{{ .Values.controller.componentName }}"
+    "app.kubernetes.io/instance": "{{ .Release.Name }}"
+  type: {{ .Values.controller.agentListenerServiceType }}
+  {{if eq .Values.controller.agentListenerServiceType "LoadBalancer"}}
+{{- if .Values.controller.agentListenerLoadBalancerSourceRanges }}
+  loadBalancerSourceRanges:
+{{ toYaml .Values.controller.agentListenerLoadBalancerSourceRanges | indent 4 }}
+{{- end }}
+  {{- end }}
+  {{- if and (eq .Values.controller.agentListenerServiceType "LoadBalancer") (.Values.controller.agentListenerLoadBalancerIP) }}
+  loadBalancerIP: {{  .Values.controller.agentListenerLoadBalancerIP }}
+  {{- end }}
+  {{- end }}
diff --git a/charts/jenkins/templates/jenkins-aws-security-group-policies.yaml b/charts/jenkins/templates/jenkins-aws-security-group-policies.yaml
new file mode 100644
index 0000000..2f6e7a1
--- /dev/null
+++ b/charts/jenkins/templates/jenkins-aws-security-group-policies.yaml
@@ -0,0 +1,16 @@
+{{- if .Values.awsSecurityGroupPolicies.enabled -}}
+{{- range .Values.awsSecurityGroupPolicies.policies -}}
+apiVersion: vpcresources.k8s.aws/v1beta1
+kind: SecurityGroupPolicy
+metadata:
+  name: {{ .name }}
+  namespace: {{ template "jenkins.namespace" $ }}
+spec:
+  podSelector: 
+    {{- toYaml .podSelector | nindent 6}}
+  securityGroups:
+    groupIds: 
+    {{- toYaml .securityGroupIds | nindent 6}}
+---
+{{- end -}}
+{{- end -}}
diff --git a/charts/jenkins/templates/jenkins-controller-alerting-rules.yaml b/charts/jenkins/templates/jenkins-controller-alerting-rules.yaml
new file mode 100644
index 0000000..3fd8061
--- /dev/null
+++ b/charts/jenkins/templates/jenkins-controller-alerting-rules.yaml
@@ -0,0 +1,26 @@
+{{- if and .Values.controller.prometheus.enabled .Values.controller.prometheus.alertingrules }}
+---
+apiVersion: monitoring.coreos.com/v1
+kind: PrometheusRule
+metadata:
+  name: {{ template "jenkins.fullname" . }}
+{{- if .Values.controller.prometheus.prometheusRuleNamespace }}
+  namespace: {{ .Values.controller.prometheus.prometheusRuleNamespace }}
+{{- else }}
+  namespace: {{ template "jenkins.namespace" . }}
+{{- end }}
+  labels:
+    "app.kubernetes.io/name": '{{ template "jenkins.name" .}}'
+    {{- if .Values.renderHelmLabels }}
+    "helm.sh/chart": "{{ template "jenkins.label" .}}"
+    {{- end }}
+    "app.kubernetes.io/managed-by": "{{ .Release.Service }}"
+    "app.kubernetes.io/instance": "{{ .Release.Name }}"
+    "app.kubernetes.io/component": "{{ .Values.controller.componentName }}"
+    {{- range $key, $val := .Values.controller.prometheus.alertingRulesAdditionalLabels }}
+    {{ $key }}: {{ $val | quote }}
+    {{- end}}
+spec:
+  groups:
+{{ toYaml .Values.controller.prometheus.alertingrules | indent 2 }}
+{{- end }}
diff --git a/charts/jenkins/templates/jenkins-controller-backendconfig.yaml b/charts/jenkins/templates/jenkins-controller-backendconfig.yaml
new file mode 100644
index 0000000..0e8a566
--- /dev/null
+++ b/charts/jenkins/templates/jenkins-controller-backendconfig.yaml
@@ -0,0 +1,24 @@
+{{- if .Values.controller.backendconfig.enabled }}
+apiVersion: {{ .Values.controller.backendconfig.apiVersion }}
+kind: BackendConfig
+metadata:
+  name: {{ .Values.controller.backendconfig.name }}
+  namespace: {{ template "jenkins.namespace" . }}
+  labels:
+    "app.kubernetes.io/name": '{{ template "jenkins.name" .}}'
+    {{- if .Values.renderHelmLabels }}
+    "helm.sh/chart": "{{ template "jenkins.label" .}}"
+    {{- end }}
+    "app.kubernetes.io/managed-by": "{{ .Release.Service }}"
+    "app.kubernetes.io/instance": "{{ .Release.Name }}"
+    "app.kubernetes.io/component": "{{ .Values.controller.componentName }}"
+{{- if .Values.controller.backendconfig.labels }}
+{{ toYaml .Values.controller.backendconfig.labels | indent 4 }}
+{{- end }}
+{{- if .Values.controller.backendconfig.annotations }}
+  annotations:
+{{ toYaml .Values.controller.backendconfig.annotations | indent 4 }}
+{{- end }}
+spec:
+{{ toYaml .Values.controller.backendconfig.spec | indent 2 }}
+{{- end }}
diff --git a/charts/jenkins/templates/jenkins-controller-ingress.yaml b/charts/jenkins/templates/jenkins-controller-ingress.yaml
new file mode 100644
index 0000000..b3b344f
--- /dev/null
+++ b/charts/jenkins/templates/jenkins-controller-ingress.yaml
@@ -0,0 +1,77 @@
+{{- $kubeTargetVersion := default .Capabilities.KubeVersion.GitVersion .Values.kubeTargetVersionOverride }}
+{{- if .Values.controller.ingress.enabled }}
+{{- if semverCompare ">=1.19-0" $kubeTargetVersion -}}
+apiVersion: networking.k8s.io/v1
+{{- else if semverCompare ">=1.14-0" $kubeTargetVersion -}}
+apiVersion: networking.k8s.io/v1beta1
+{{- else -}}
+apiVersion: {{ .Values.controller.ingress.apiVersion }}
+{{- end }}
+kind: Ingress
+metadata:
+  namespace: {{ template "jenkins.namespace" . }}
+  labels:
+    "app.kubernetes.io/name": '{{ template "jenkins.name" .}}'
+    {{- if .Values.renderHelmLabels }}
+    "helm.sh/chart": "{{ template "jenkins.label" .}}"
+    {{- end }}
+    "app.kubernetes.io/managed-by": "{{ .Release.Service }}"
+    "app.kubernetes.io/instance": "{{ .Release.Name }}"
+    "app.kubernetes.io/component": "{{ .Values.controller.componentName }}"
+{{- if .Values.controller.ingress.labels }}
+{{ toYaml .Values.controller.ingress.labels | indent 4 }}
+{{- end }}
+{{- if .Values.controller.ingress.annotations }}
+  annotations:
+{{ toYaml .Values.controller.ingress.annotations | indent 4 }}
+{{- end }}
+  name: {{ template "jenkins.fullname" . }}
+spec:
+{{- if .Values.controller.ingress.ingressClassName }}
+  ingressClassName: {{ .Values.controller.ingress.ingressClassName | quote }}
+{{- end }}
+  rules:
+  - http:
+      paths:
+{{- if empty (.Values.controller.ingress.paths) }}
+      - backend:
+{{- if semverCompare ">=1.19-0" $kubeTargetVersion }}
+          service:
+            name: {{ template "jenkins.fullname" . }}
+            port:
+              number: {{ .Values.controller.servicePort }}
+        pathType: ImplementationSpecific
+{{- else }}
+          serviceName: {{ template "jenkins.fullname" . }}
+          servicePort: {{ .Values.controller.servicePort }}
+{{- end }}
+{{- if .Values.controller.ingress.path }}
+        path: {{ .Values.controller.ingress.path }}
+{{- end -}}
+{{- else }}
+{{ tpl (toYaml .Values.controller.ingress.paths | indent 6) . }}
+{{- end -}}
+{{- if .Values.controller.ingress.hostName }}
+    host: {{ tpl .Values.controller.ingress.hostName . | quote }}
+{{- end }}
+{{- if .Values.controller.ingress.resourceRootUrl }}
+  - http:
+      paths:
+      - backend:
+{{- if semverCompare ">=1.19-0" $kubeTargetVersion }}
+          service:
+            name: {{ template "jenkins.fullname" . }}
+            port:
+              number: {{ .Values.controller.servicePort }}
+        pathType: ImplementationSpecific
+{{- else }}
+          serviceName: {{ template "jenkins.fullname" . }}
+          servicePort: {{ .Values.controller.servicePort }}
+{{- end }}
+    host: {{ tpl .Values.controller.ingress.resourceRootUrl . | quote }}
+{{- end }}
+{{- if .Values.controller.ingress.tls }}
+  tls:
+{{ tpl (toYaml .Values.controller.ingress.tls ) . | indent 4 }}
+{{- end -}}
+{{- end }}
diff --git a/charts/jenkins/templates/jenkins-controller-networkpolicy.yaml b/charts/jenkins/templates/jenkins-controller-networkpolicy.yaml
new file mode 100644
index 0000000..82835f2
--- /dev/null
+++ b/charts/jenkins/templates/jenkins-controller-networkpolicy.yaml
@@ -0,0 +1,76 @@
+{{- if .Values.networkPolicy.enabled }}
+kind: NetworkPolicy
+apiVersion: {{ .Values.networkPolicy.apiVersion }}
+metadata:
+  name: "{{ .Release.Name }}-{{ .Values.controller.componentName }}"
+  namespace: {{ template "jenkins.namespace" . }}
+  labels:
+    "app.kubernetes.io/name": '{{ template "jenkins.name" .}}'
+    {{- if .Values.renderHelmLabels }}
+    "helm.sh/chart": "{{ template "jenkins.label" .}}"
+    {{- end }}
+    "app.kubernetes.io/managed-by": "{{ .Release.Service }}"
+    "app.kubernetes.io/instance": "{{ .Release.Name }}"
+    "app.kubernetes.io/component": "{{ .Values.controller.componentName }}"
+spec:
+  podSelector:
+    matchLabels:
+      "app.kubernetes.io/component": "{{ .Values.controller.componentName }}"
+      "app.kubernetes.io/instance": "{{ .Release.Name }}"
+  ingress:
+    # Allow web access to the UI
+    - ports:
+      - port: {{ .Values.controller.targetPort }}
+    {{- if .Values.controller.agentListenerEnabled }}
+    # Allow inbound connections from agents
+    - from:
+      {{- if .Values.networkPolicy.internalAgents.allowed }}
+      - podSelector:
+          matchLabels:
+            "jenkins/{{ .Release.Name }}-{{ .Values.agent.componentName }}": "true"
+            {{- range $k,$v:= .Values.networkPolicy.internalAgents.podLabels }}
+            {{ $k }}: {{ $v }}
+            {{- end }}
+        {{- if .Values.networkPolicy.internalAgents.namespaceLabels }}
+        namespaceSelector:
+          matchLabels:
+            {{- range $k,$v:= .Values.networkPolicy.internalAgents.namespaceLabels }}
+            {{ $k }}: {{ $v }}
+            {{- end }}
+        {{- end }}
+      {{- end }}
+      {{- if or .Values.networkPolicy.externalAgents.ipCIDR .Values.networkPolicy.externalAgents.except }}
+      - ipBlock:
+          cidr: {{ required "ipCIDR is required if you wish to allow external agents to connect to Jenkins Controller." .Values.networkPolicy.externalAgents.ipCIDR }}
+          {{- if .Values.networkPolicy.externalAgents.except }}
+          except:
+          {{- range .Values.networkPolicy.externalAgents.except }}
+          - {{ . }}
+          {{- end }}
+          {{- end }}
+      {{- end }}
+      ports:
+      - port: {{ .Values.controller.agentListenerPort }}
+    {{- end }}
+{{- if .Values.agent.enabled }}
+---
+kind: NetworkPolicy
+apiVersion: {{ .Values.networkPolicy.apiVersion }}
+metadata:
+  name: "{{ .Release.Name }}-{{ .Values.agent.componentName }}"
+  namespace: {{ template "jenkins.namespace" . }}
+  labels:
+    "app.kubernetes.io/name": '{{ template "jenkins.name" .}}'
+    {{- if .Values.renderHelmLabels }}
+    "helm.sh/chart": "{{ template "jenkins.label" .}}"
+    {{- end }}
+    "app.kubernetes.io/managed-by": "{{ .Release.Service }}"
+    "app.kubernetes.io/instance": "{{ .Release.Name }}"
+    "app.kubernetes.io/component": "{{ .Values.controller.componentName }}"
+spec:
+  podSelector:
+    matchLabels:
+      # DefaultDeny
+      "jenkins/{{ .Release.Name }}-{{ .Values.agent.componentName }}": "true"
+{{- end }}
+{{- end }}
diff --git a/charts/jenkins/templates/jenkins-controller-pdb.yaml b/charts/jenkins/templates/jenkins-controller-pdb.yaml
new file mode 100644
index 0000000..9dc1faf
--- /dev/null
+++ b/charts/jenkins/templates/jenkins-controller-pdb.yaml
@@ -0,0 +1,34 @@
+{{- if .Values.controller.podDisruptionBudget.enabled }}
+{{- $kubeTargetVersion := default .Capabilities.KubeVersion.GitVersion .Values.kubeTargetVersionOverride }}
+{{- if semverCompare ">=1.21-0" $kubeTargetVersion -}}
+apiVersion: policy/v1
+{{- else if semverCompare ">=1.5-0" $kubeTargetVersion -}}
+apiVersion: policy/v1beta1
+{{- else -}}
+apiVersion: {{ .Values.controller.podDisruptionBudget.apiVersion }}
+{{- end }}
+kind: PodDisruptionBudget
+metadata:
+  name: {{ template "jenkins.fullname" . }}-pdb
+  namespace: {{ template "jenkins.namespace" . }}
+  labels:
+    "app.kubernetes.io/name": '{{ template "jenkins.name" .}}'
+    {{- if .Values.renderHelmLabels }}
+    "helm.sh/chart": "{{ template "jenkins.label" .}}"
+    {{- end }}
+    "app.kubernetes.io/managed-by": "{{ .Release.Service }}"
+    "app.kubernetes.io/instance": "{{ .Release.Name }}"
+    "app.kubernetes.io/component": "{{ .Values.controller.componentName }}"
+    {{- if .Values.controller.podDisruptionBudget.labels -}}
+    {{ toYaml .Values.controller.podDisruptionBudget.labels | nindent 4 }}
+    {{- end }}
+  {{- if .Values.controller.podDisruptionBudget.annotations }}
+  annotations: {{ toYaml .Values.controller.podDisruptionBudget.annotations | nindent 4 }}
+  {{- end }}
+spec:
+  maxUnavailable: {{ .Values.controller.podDisruptionBudget.maxUnavailable }}
+  selector:
+    matchLabels:
+      "app.kubernetes.io/instance": "{{ .Release.Name }}"
+      "app.kubernetes.io/name": '{{ template "jenkins.name" .}}'
+{{- end }}
diff --git a/charts/jenkins/templates/jenkins-controller-podmonitor.yaml b/charts/jenkins/templates/jenkins-controller-podmonitor.yaml
new file mode 100644
index 0000000..9a04019
--- /dev/null
+++ b/charts/jenkins/templates/jenkins-controller-podmonitor.yaml
@@ -0,0 +1,30 @@
+{{- if .Values.controller.googlePodMonitor.enabled }}
+apiVersion: monitoring.googleapis.com/v1
+kind: PodMonitoring
+
+metadata:
+  name: {{ template "jenkins.fullname" . }}
+{{- if .Values.controller.googlePodMonitor.serviceMonitorNamespace }}
+  namespace: {{ .Values.controller.googlePodMonitor.serviceMonitorNamespace }}
+{{- else }}
+  namespace: {{ template "jenkins.namespace" . }}
+{{- end }}
+  labels:
+    "app.kubernetes.io/name": '{{ template "jenkins.name" .}}'
+    {{- if .Values.renderHelmLabels }}
+    "helm.sh/chart": "{{ template "jenkins.label" .}}"
+    {{- end }}
+    "app.kubernetes.io/managed-by": "{{ .Release.Service }}"
+    "app.kubernetes.io/instance": "{{ .Release.Name }}"
+    "app.kubernetes.io/component": "{{ .Values.controller.componentName }}"
+
+spec:
+  endpoints:
+  - interval: {{ .Values.controller.googlePodMonitor.scrapeInterval }}
+    port: http
+    path: {{ .Values.controller.jenkinsUriPrefix }}{{ .Values.controller.googlePodMonitor.scrapeEndpoint }}
+  selector:
+    matchLabels:
+      "app.kubernetes.io/instance": "{{ .Release.Name }}"
+      "app.kubernetes.io/component": "{{ .Values.controller.componentName }}"
+{{- end }}
diff --git a/charts/jenkins/templates/jenkins-controller-route.yaml b/charts/jenkins/templates/jenkins-controller-route.yaml
new file mode 100644
index 0000000..3550380
--- /dev/null
+++ b/charts/jenkins/templates/jenkins-controller-route.yaml
@@ -0,0 +1,34 @@
+{{- if .Values.controller.route.enabled }}
+apiVersion: route.openshift.io/v1
+kind: Route
+metadata:
+  namespace: {{ template "jenkins.namespace" . }}
+  labels:
+    app: {{ template "jenkins.fullname" . }}
+    {{- if .Values.renderHelmLabels }}
+    "helm.sh/chart": "{{ template "jenkins.label" .}}"
+    {{- end }}
+    release: "{{ .Release.Name }}"
+    heritage: "{{ .Release.Service }}"
+    component: "{{ .Release.Name }}-{{ .Values.controller.componentName }}"
+{{- if .Values.controller.route.labels }}
+{{ toYaml .Values.controller.route.labels | indent 4 }}
+{{- end }}
+{{- if .Values.controller.route.annotations }}
+  annotations:
+{{ toYaml .Values.controller.route.annotations | indent 4 }}
+{{- end }}
+  name: {{ template "jenkins.fullname" . }}
+spec:
+  host: {{ .Values.controller.route.path }}
+  port:
+    targetPort: http
+  tls:
+    insecureEdgeTerminationPolicy: Redirect
+    termination: edge
+  to:
+    kind: Service
+    name: {{ template "jenkins.fullname" . }}
+    weight: 100
+  wildcardPolicy: None
+{{- end }}
diff --git a/charts/jenkins/templates/jenkins-controller-secondary-ingress.yaml b/charts/jenkins/templates/jenkins-controller-secondary-ingress.yaml
new file mode 100644
index 0000000..c63e482
--- /dev/null
+++ b/charts/jenkins/templates/jenkins-controller-secondary-ingress.yaml
@@ -0,0 +1,56 @@
+{{- if .Values.controller.secondaryingress.enabled }}
+{{- $kubeTargetVersion := default .Capabilities.KubeVersion.GitVersion .Values.kubeTargetVersionOverride }}
+{{- $serviceName := include "jenkins.fullname" . -}}
+{{- $servicePort := .Values.controller.servicePort -}}
+{{- if semverCompare ">=1.19-0" $kubeTargetVersion -}}
+apiVersion: networking.k8s.io/v1
+{{- else if semverCompare ">=1.14-0" $kubeTargetVersion -}}
+apiVersion: networking.k8s.io/v1beta1
+{{- else -}}
+apiVersion: {{ .Values.controller.secondaryingress.apiVersion }}
+{{- end }}
+kind: Ingress
+metadata:
+  namespace: {{ template "jenkins.namespace" . }}
+  labels:
+    "app.kubernetes.io/name": '{{ template "jenkins.name" .}}'
+    {{- if .Values.renderHelmLabels }}
+    "helm.sh/chart": "{{ template "jenkins.label" .}}"
+    {{- end }}
+    "app.kubernetes.io/managed-by": "{{ .Release.Service }}"
+    "app.kubernetes.io/instance": "{{ .Release.Name }}"
+    "app.kubernetes.io/component": "{{ .Values.controller.componentName }}"
+    {{- if .Values.controller.secondaryingress.labels -}}
+    {{ toYaml .Values.controller.secondaryingress.labels | nindent 4 }}
+    {{- end }}
+  {{- if .Values.controller.secondaryingress.annotations }}
+  annotations: {{ toYaml .Values.controller.secondaryingress.annotations | nindent 4 }}
+  {{- end }}
+  name: {{ template "jenkins.fullname" . }}-secondary
+spec:
+{{- if .Values.controller.secondaryingress.ingressClassName }}
+  ingressClassName: {{ .Values.controller.secondaryingress.ingressClassName | quote }}
+{{- end }}
+  rules:
+    - host: {{ .Values.controller.secondaryingress.hostName }}
+      http:
+        paths:
+        {{- range .Values.controller.secondaryingress.paths }}
+          - path: {{ . | quote }}
+            backend:
+{{ if semverCompare ">=1.19-0" $kubeTargetVersion }}
+              service:
+                name: {{ $serviceName }}
+                port:
+                  number: {{ $servicePort }}
+            pathType: ImplementationSpecific
+{{ else }}
+              serviceName: {{ $serviceName }}
+              servicePort: {{ $servicePort }}
+{{ end }}
+        {{- end}}
+{{- if .Values.controller.secondaryingress.tls }}
+  tls:
+{{ toYaml .Values.controller.secondaryingress.tls | indent 4 }}
+{{- end -}}
+{{- end }}
diff --git a/charts/jenkins/templates/jenkins-controller-servicemonitor.yaml b/charts/jenkins/templates/jenkins-controller-servicemonitor.yaml
new file mode 100644
index 0000000..8710b2b
--- /dev/null
+++ b/charts/jenkins/templates/jenkins-controller-servicemonitor.yaml
@@ -0,0 +1,45 @@
+{{- if and .Values.controller.prometheus.enabled }}
+apiVersion: monitoring.coreos.com/v1
+kind: ServiceMonitor
+
+metadata:
+  name: {{ template "jenkins.fullname" . }}
+{{- if .Values.controller.prometheus.serviceMonitorNamespace }}
+  namespace: {{ .Values.controller.prometheus.serviceMonitorNamespace }}
+{{- else }}
+  namespace: {{ template "jenkins.namespace" . }}
+{{- end }}
+  labels:
+    "app.kubernetes.io/name": '{{ template "jenkins.name" .}}'
+    {{- if .Values.renderHelmLabels }}
+    "helm.sh/chart": "{{ template "jenkins.label" .}}"
+    {{- end }}
+    "app.kubernetes.io/managed-by": "{{ .Release.Service }}"
+    "app.kubernetes.io/instance": "{{ .Release.Name }}"
+    "app.kubernetes.io/component": "{{ .Values.controller.componentName }}"
+    {{- range $key, $val := .Values.controller.prometheus.serviceMonitorAdditionalLabels }}
+    {{ $key }}: {{ $val | quote }}
+    {{- end}}
+
+spec:
+  endpoints:
+  - interval: {{ .Values.controller.prometheus.scrapeInterval }}
+    port: http
+    path: {{ .Values.controller.jenkinsUriPrefix }}{{ .Values.controller.prometheus.scrapeEndpoint }}
+    {{- with .Values.controller.prometheus.relabelings }}
+    relabelings:
+      {{- toYaml . | nindent 6 }}
+    {{- end }}
+    {{- with .Values.controller.prometheus.metricRelabelings }}
+    metricRelabelings:
+      {{- toYaml . | nindent 6 }}
+    {{- end }}
+  jobLabel: {{ template "jenkins.fullname" . }}
+  namespaceSelector:
+    matchNames:
+    - "{{ template "jenkins.namespace" $ }}"
+  selector:
+    matchLabels:
+      "app.kubernetes.io/instance": "{{ .Release.Name }}"
+      "app.kubernetes.io/component": "{{ .Values.controller.componentName }}"
+{{- end }}
diff --git a/charts/jenkins/templates/jenkins-controller-statefulset.yaml b/charts/jenkins/templates/jenkins-controller-statefulset.yaml
new file mode 100644
index 0000000..ca0edc6
--- /dev/null
+++ b/charts/jenkins/templates/jenkins-controller-statefulset.yaml
@@ -0,0 +1,413 @@
+{{- if .Capabilities.APIVersions.Has "apps/v1" }}
+apiVersion: apps/v1
+{{- else }}
+apiVersion: apps/v1beta1
+{{- end }}
+kind: StatefulSet
+metadata:
+  name: {{ template "jenkins.fullname" . }}
+  namespace: {{ template "jenkins.namespace" . }}
+  labels:
+    "app.kubernetes.io/name": '{{ template "jenkins.name" .}}'
+    {{- if .Values.renderHelmLabels }}
+    "helm.sh/chart": "{{ template "jenkins.label" .}}"
+    {{- end }}
+    "app.kubernetes.io/managed-by": "{{ .Release.Service }}"
+    "app.kubernetes.io/instance": "{{ .Release.Name }}"
+    "app.kubernetes.io/component": "{{ .Values.controller.componentName }}"
+    {{- range $key, $val := .Values.controller.statefulSetLabels }}
+    {{ $key }}: {{ $val | quote }}
+    {{- end}}
+  {{- if .Values.controller.statefulSetAnnotations }}
+  annotations:
+{{ toYaml .Values.controller.statefulSetAnnotations | indent 4 }}
+  {{- end }}
+spec:
+  serviceName: {{ template "jenkins.fullname" . }}
+  replicas: 1
+  selector:
+    matchLabels:
+      "app.kubernetes.io/component": "{{ .Values.controller.componentName }}"
+      "app.kubernetes.io/instance": "{{ .Release.Name }}"
+  {{- if .Values.controller.updateStrategy }}
+  updateStrategy:
+{{ toYaml .Values.controller.updateStrategy | indent 4 }}
+  {{- end }}
+  template:
+    metadata:
+      labels:
+        "app.kubernetes.io/name": '{{ template "jenkins.name" .}}'
+        "app.kubernetes.io/managed-by": "{{ .Release.Service }}"
+        "app.kubernetes.io/instance": "{{ .Release.Name }}"
+        "app.kubernetes.io/component": "{{ .Values.controller.componentName }}"
+        {{- range $key, $val := .Values.controller.podLabels }}
+        {{ $key }}: {{ $val | quote }}
+        {{- end}}
+      annotations:
+        checksum/config: {{ include (print $.Template.BasePath "/config.yaml") . | sha256sum }}
+        {{- if .Values.controller.initScripts }}
+        checksum/config-init-scripts: {{ include (print $.Template.BasePath "/config-init-scripts.yaml") . | sha256sum }}
+        {{- end }}
+        {{- if .Values.controller.podAnnotations }}
+{{ tpl (toYaml .Values.controller.podAnnotations | indent 8) . }}
+        {{- end }}
+    spec:
+      {{- if .Values.controller.schedulerName }}
+      schedulerName: {{ .Values.controller.schedulerName }}
+      {{- end }}
+      {{- if .Values.controller.nodeSelector }}
+      nodeSelector:
+{{ toYaml .Values.controller.nodeSelector | indent 8 }}
+      {{- end }}
+      {{- if .Values.controller.tolerations }}
+      tolerations:
+{{ toYaml .Values.controller.tolerations | indent 8 }}
+      {{- end }}
+      {{- if .Values.controller.affinity }}
+      affinity:
+{{ toYaml .Values.controller.affinity | indent 8 }}
+      {{- end }}
+      {{- if quote .Values.controller.terminationGracePeriodSeconds }}
+      terminationGracePeriodSeconds: {{ .Values.controller.terminationGracePeriodSeconds }}
+      {{- end }}
+      {{- if .Values.controller.priorityClassName }}
+      priorityClassName: {{ .Values.controller.priorityClassName }}
+      {{- end }}
+      {{- if .Values.controller.shareProcessNamespace }}
+      shareProcessNamespace: true
+      {{- end }}
+{{- if .Values.controller.usePodSecurityContext }}
+      securityContext:
+  {{- if kindIs "map" .Values.controller.podSecurityContextOverride }}
+    {{- tpl (toYaml .Values.controller.podSecurityContextOverride | nindent 8) . -}}
+  {{- else }}
+    {{/* The rest of this section should be replaced with the contents of this comment one the runAsUser, fsGroup, and securityContextCapabilities Helm chart values have been removed:
+        runAsUser: 1000
+        fsGroup: 1000
+        runAsNonRoot: true
+    */}}
+        runAsUser: {{ default 0 .Values.controller.runAsUser }}
+    {{- if and (.Values.controller.runAsUser) (.Values.controller.fsGroup) }}
+      {{- if not (eq (int .Values.controller.runAsUser) 0) }}
+        fsGroup: {{ .Values.controller.fsGroup }}
+        runAsNonRoot: true
+      {{- end }}
+      {{- if .Values.controller.securityContextCapabilities }}
+        capabilities:
+          {{- toYaml .Values.controller.securityContextCapabilities | nindent 10 }}
+      {{- end }}
+    {{- end }}
+  {{- end }}
+{{- end }}
+      serviceAccountName: "{{ template "jenkins.serviceAccountName" . }}"
+{{- if .Values.controller.hostNetworking }}
+      hostNetwork: true
+      dnsPolicy: ClusterFirstWithHostNet
+{{- end }}
+      {{- if .Values.controller.hostAliases }}
+      hostAliases:
+        {{- toYaml .Values.controller.hostAliases | nindent 8 }}
+      {{- end }}
+      initContainers:
+{{- if .Values.controller.customInitContainers }}
+{{ tpl (toYaml .Values.controller.customInitContainers) . | indent 8 }}
+{{- end }}
+
+{{- if .Values.controller.sidecars.configAutoReload.enabled }}
+{{- include "jenkins.configReloadContainer" (list $ "config-reload-init" "init") | nindent 8 }}
+{{- end}}
+
+        - name: "init"
+          image: "{{ .Values.controller.image.registry }}/{{ .Values.controller.image.repository }}:{{- include "controller.image.tag" . -}}"
+          imagePullPolicy: "{{ .Values.controller.image.pullPolicy }}"
+          {{- if .Values.controller.containerSecurityContext }}
+          securityContext: {{- toYaml .Values.controller.containerSecurityContext | nindent 12 }}
+          {{- end }}
+          command: [ "sh", "/var/jenkins_config/apply_config.sh" ]
+            {{- if .Values.controller.initContainerEnvFrom }}
+          envFrom:
+{{ (tpl (toYaml .Values.controller.initContainerEnvFrom) .) | indent 12 }}
+            {{- end }}
+            {{- if .Values.controller.initContainerEnv }}
+          env:
+{{ (tpl (toYaml .Values.controller.initContainerEnv) .) | indent 12 }}
+            {{- end }}
+          resources:
+{{- if .Values.controller.initContainerResources }}
+{{ toYaml .Values.controller.initContainerResources | indent 12 }}
+{{- else }}
+{{ toYaml .Values.controller.resources | indent 12 }}
+{{- end }}
+          volumeMounts:
+            {{- if .Values.persistence.mounts }}
+{{ toYaml .Values.persistence.mounts | indent 12 }}
+            {{- end }}
+            - mountPath: {{ .Values.controller.jenkinsHome }}
+              name: jenkins-home
+              {{- if .Values.persistence.subPath }}
+              subPath: {{ .Values.persistence.subPath }}
+              {{- end }}
+            - mountPath: /var/jenkins_config
+              name: jenkins-config
+            {{- if .Values.controller.installPlugins }}
+            {{- if .Values.controller.overwritePluginsFromImage }}
+            - mountPath: {{ .Values.controller.jenkinsRef }}/plugins
+              name: plugins
+            {{- end }}
+            - mountPath: /var/jenkins_plugins
+              name: plugin-dir
+            - mountPath: /tmp
+              name: tmp-volume
+            {{- end }}
+            {{- if or .Values.controller.initScripts .Values.controller.initConfigMap }}
+            - mountPath: {{ .Values.controller.jenkinsHome }}/init.groovy.d
+              name: init-scripts
+            {{- end }}
+            {{- if and .Values.controller.httpsKeyStore.enable (not .Values.controller.httpsKeyStore.disableSecretMount) }}
+            {{- $httpsJKSDirPath :=  printf "%s" .Values.controller.httpsKeyStore.path }}
+            - mountPath: {{ $httpsJKSDirPath }}
+              name: jenkins-https-keystore
+            {{- end }}
+      containers:
+        - name: jenkins
+          image: "{{ .Values.controller.image.registry }}/{{ .Values.controller.image.repository }}:{{- include "controller.image.tag" . -}}"
+          imagePullPolicy: "{{ .Values.controller.image.pullPolicy }}"
+          {{- if .Values.controller.containerSecurityContext }}
+          securityContext: {{- toYaml .Values.controller.containerSecurityContext | nindent 12 }}
+          {{- end }}
+          {{- if .Values.controller.overrideArgs }}
+          args: [
+            {{- range $overrideArg := .Values.controller.overrideArgs }}
+              "{{- tpl $overrideArg $ }}",
+            {{- end }}
+          ]
+          {{- else if .Values.controller.httpsKeyStore.enable }}
+          {{- $httpsJKSFilePath :=  printf "%s/%s" .Values.controller.httpsKeyStore.path .Values.controller.httpsKeyStore.fileName }}
+          args: [ "--httpPort={{.Values.controller.httpsKeyStore.httpPort}}", "--httpsPort={{.Values.controller.targetPort}}", '--httpsKeyStore={{ $httpsJKSFilePath }}', "--httpsKeyStorePassword=$(JENKINS_HTTPS_KEYSTORE_PASSWORD)" ]
+          {{- else }}
+          args: [ "--httpPort={{.Values.controller.targetPort}}"]
+          {{- end }}
+          {{- if .Values.controller.lifecycle }}
+          lifecycle:
+{{ toYaml .Values.controller.lifecycle | indent 12 }}
+          {{- end }}
+{{- if .Values.controller.terminationMessagePath }}
+          terminationMessagePath: {{ .Values.controller.terminationMessagePath }}
+{{- end }}
+{{- if .Values.controller.terminationMessagePolicy }}
+          terminationMessagePolicy: {{ .Values.controller.terminationMessagePolicy }}
+{{- end }}
+            {{- if .Values.controller.containerEnvFrom }}
+          envFrom:
+{{ (tpl ( toYaml .Values.controller.containerEnvFrom) .) | indent 12 }}
+            {{- end }}
+          env:
+            {{- if .Values.controller.containerEnv }}
+{{ (tpl ( toYaml .Values.controller.containerEnv) .) | indent 12 }}
+            {{- end }}
+            {{- if or .Values.controller.additionalSecrets .Values.controller.existingSecret .Values.controller.additionalExistingSecrets .Values.controller.admin.createSecret }}
+            - name: SECRETS
+              value: /run/secrets/additional
+            {{- end }}
+            - name: POD_NAME
+              valueFrom:
+                fieldRef:
+                  fieldPath: metadata.name
+            - name: JAVA_OPTS
+              value: >-
+                {{ if .Values.controller.sidecars.configAutoReload.enabled }} -Dcasc.reload.token=$(POD_NAME) {{ end }}{{ default "" .Values.controller.javaOpts }}
+            - name: JENKINS_OPTS
+              value: >-
+                {{ if .Values.controller.jenkinsUriPrefix }}--prefix={{ .Values.controller.jenkinsUriPrefix }} {{ end }} --webroot=/var/jenkins_cache/war {{ default "" .Values.controller.jenkinsOpts}}
+            - name: JENKINS_SLAVE_AGENT_PORT
+              value: "{{ .Values.controller.agentListenerPort }}"
+            {{- if .Values.controller.httpsKeyStore.enable }}
+            - name: JENKINS_HTTPS_KEYSTORE_PASSWORD
+            {{- if not .Values.controller.httpsKeyStore.disableSecretMount }}
+              valueFrom:
+                secretKeyRef:
+                  name: {{ if .Values.controller.httpsKeyStore.jenkinsHttpsJksPasswordSecretName }} {{ .Values.controller.httpsKeyStore.jenkinsHttpsJksPasswordSecretName }} {{ else if .Values.controller.httpsKeyStore.jenkinsHttpsJksSecretName }} {{ .Values.controller.httpsKeyStore.jenkinsHttpsJksSecretName }} {{ else }} {{ template "jenkins.fullname" . }}-https-jks  {{ end }}
+                  key: "{{ .Values.controller.httpsKeyStore.jenkinsHttpsJksPasswordSecretKey }}"
+            {{- else }}
+              value: {{ .Values.controller.httpsKeyStore.password }}
+            {{- end }}
+            {{- end }}
+
+            - name: CASC_JENKINS_CONFIG
+              value: {{ .Values.controller.sidecars.configAutoReload.folder | default (printf "%s/casc_configs" (.Values.controller.jenkinsRef)) }}{{- if .Values.controller.JCasC.configUrls }},{{ join "," .Values.controller.JCasC.configUrls }}{{- end }}
+          ports:
+            {{- if .Values.controller.httpsKeyStore.enable }}
+            - containerPort: {{.Values.controller.httpsKeyStore.httpPort}}
+            {{- else }}
+            - containerPort: {{.Values.controller.targetPort}}
+            {{- end }}
+              name: http
+            - containerPort: {{ .Values.controller.agentListenerPort }}
+              name: agent-listener
+              {{- if .Values.controller.agentListenerHostPort }}
+              hostPort: {{ .Values.controller.agentListenerHostPort }}
+              {{- end }}
+            {{- if .Values.controller.jmxPort }}
+            - containerPort: {{ .Values.controller.jmxPort }}
+              name: jmx
+            {{- end }}
+{{- range $index, $port := .Values.controller.extraPorts }}
+            - containerPort: {{ $port.port }}
+              name: {{ $port.name }}
+{{- end }}
+{{- if and .Values.controller.healthProbes .Values.controller.probes}}
+  {{- if semverCompare ">=1.16-0" .Capabilities.KubeVersion.GitVersion }}
+          startupProbe:
+{{ tpl (toYaml .Values.controller.probes.startupProbe | indent 12) .}}
+  {{- end }}
+          livenessProbe:
+{{ tpl (toYaml .Values.controller.probes.livenessProbe | indent 12) .}}
+          readinessProbe:
+{{ tpl (toYaml .Values.controller.probes.readinessProbe | indent 12) .}}
+{{- end }}
+          resources:
+{{ toYaml .Values.controller.resources | indent 12 }}
+          volumeMounts:
+{{- if .Values.persistence.mounts }}
+{{ toYaml .Values.persistence.mounts | indent 12 }}
+{{- end }}
+            {{- if and .Values.controller.httpsKeyStore.enable (not .Values.controller.httpsKeyStore.disableSecretMount) }}
+            {{- $httpsJKSDirPath :=  printf "%s" .Values.controller.httpsKeyStore.path }}
+            - mountPath: {{ $httpsJKSDirPath }}
+              name: jenkins-https-keystore
+            {{- end }}
+            - mountPath: {{ .Values.controller.jenkinsHome }}
+              name: jenkins-home
+              readOnly: false
+              {{- if .Values.persistence.subPath }}
+              subPath: {{ .Values.persistence.subPath }}
+              {{- end }}
+            - mountPath: /var/jenkins_config
+              name: jenkins-config
+              readOnly: true
+            {{- if .Values.controller.installPlugins }}
+            - mountPath: {{ .Values.controller.jenkinsRef }}/plugins/
+              name: plugin-dir
+              readOnly: false
+            {{- end }}
+            {{- if or .Values.controller.initScripts .Values.controller.initConfigMap }}
+            - mountPath: {{ .Values.controller.jenkinsHome }}/init.groovy.d
+              name: init-scripts
+            {{- end }}
+            {{- if .Values.controller.sidecars.configAutoReload.enabled }}
+            - name: sc-config-volume
+              mountPath: {{ .Values.controller.sidecars.configAutoReload.folder | default (printf "%s/casc_configs" (.Values.controller.jenkinsRef)) }}
+            {{- end }}
+            {{- if or .Values.controller.additionalSecrets .Values.controller.existingSecret .Values.controller.additionalExistingSecrets .Values.controller.admin.createSecret }}
+            - name: jenkins-secrets
+              mountPath: /run/secrets/additional
+              readOnly: true
+            {{- end }}
+            - name: jenkins-cache
+              mountPath: /var/jenkins_cache
+            - mountPath: /tmp
+              name: tmp-volume
+
+{{- if .Values.controller.sidecars.configAutoReload.enabled }}
+{{- include "jenkins.configReloadContainer" (list $ "config-reload" "sidecar") | nindent 8 }}
+{{- end}}
+
+
+{{- if .Values.controller.sidecars.additionalSidecarContainers}}
+{{ tpl (toYaml .Values.controller.sidecars.additionalSidecarContainers | indent 8) .}}
+{{- end }}
+
+      volumes:
+{{- if .Values.persistence.volumes }}
+{{ tpl (toYaml .Values.persistence.volumes | indent 6) . }}
+{{- end }}
+      {{- if .Values.controller.installPlugins }}
+      {{- if .Values.controller.overwritePluginsFromImage }}
+      - name: plugins
+        emptyDir: {}
+      {{- end }}
+      {{- end }}
+      {{- if and .Values.controller.initScripts .Values.controller.initConfigMap }}
+      - name: init-scripts
+        projected:
+          sources:
+          - configMap:
+              name: {{ template "jenkins.fullname" . }}-init-scripts
+          - configMap:
+              name: {{ .Values.controller.initConfigMap }}
+      {{- else if .Values.controller.initConfigMap }}
+      - name: init-scripts
+        configMap:
+          name: {{ .Values.controller.initConfigMap }}
+      {{- else if .Values.controller.initScripts }}
+      - name: init-scripts
+        configMap:
+          name: {{ template "jenkins.fullname" . }}-init-scripts
+      {{- end }}
+      - name: jenkins-config
+        configMap:
+          name: {{ template "jenkins.fullname" . }}
+      {{- if .Values.controller.installPlugins }}
+      - name: plugin-dir
+        emptyDir: {}
+      {{- end }}
+      {{- if or .Values.controller.additionalSecrets .Values.controller.existingSecret .Values.controller.additionalExistingSecrets .Values.controller.admin.createSecret }}
+      - name: jenkins-secrets
+        projected:
+          sources:
+          {{- if .Values.controller.additionalSecrets }}
+          - secret:
+              name: {{ template "jenkins.fullname" . }}-additional-secrets
+          {{- end }}
+          {{- if .Values.controller.additionalExistingSecrets }}
+          {{- range $key, $value := .Values.controller.additionalExistingSecrets }}
+          - secret:
+              name: {{ tpl $value.name $ }}
+              items:
+                - key: {{ tpl $value.keyName $ }}
+                  path: {{ tpl $value.name $ }}-{{ tpl $value.keyName $ }}
+          {{- end }}
+          {{- end }}
+          {{- if .Values.controller.admin.createSecret }}
+          - secret:
+              name: {{ .Values.controller.admin.existingSecret | default (include "jenkins.fullname" .) }}
+              items:
+                - key: {{ .Values.controller.admin.userKey | default "jenkins-admin-user" }}
+                  path: chart-admin-username
+                - key: {{ .Values.controller.admin.passwordKey | default "jenkins-admin-password" }}
+                  path: chart-admin-password
+        {{- end }}
+        {{- if .Values.controller.existingSecret }}
+          - secret:
+              name: {{ .Values.controller.existingSecret }}
+        {{- end }}
+      {{- end }}
+      - name: jenkins-cache
+        emptyDir: {}
+      {{- if not (contains "jenkins-home" (quote .Values.persistence.volumes)) }}
+      - name: jenkins-home
+      {{- if .Values.persistence.enabled }}
+        persistentVolumeClaim:
+          claimName: {{ .Values.persistence.existingClaim | default (include "jenkins.fullname" .) }}
+      {{- else }}
+        emptyDir: {}
+      {{- end -}}
+      {{- end }}
+      - name: sc-config-volume
+        emptyDir: {}
+      - name: tmp-volume
+        emptyDir: {}
+
+      {{- if and .Values.controller.httpsKeyStore.enable (not .Values.controller.httpsKeyStore.disableSecretMount) }}
+      - name: jenkins-https-keystore
+        secret:
+          secretName: {{ if .Values.controller.httpsKeyStore.jenkinsHttpsJksSecretName }} {{ .Values.controller.httpsKeyStore.jenkinsHttpsJksSecretName }} {{ else }} {{ template "jenkins.fullname" . }}-https-jks  {{ end }}
+          items:
+          - key: {{ .Values.controller.httpsKeyStore.jenkinsHttpsJksSecretKey }}
+            path: {{ .Values.controller.httpsKeyStore.fileName }}
+      {{- end }}
+
+{{- if .Values.controller.imagePullSecretName }}
+      imagePullSecrets:
+      - name: {{ .Values.controller.imagePullSecretName }}
+{{- end -}}
diff --git a/charts/jenkins/templates/jenkins-controller-svc.yaml b/charts/jenkins/templates/jenkins-controller-svc.yaml
new file mode 100644
index 0000000..a83466c
--- /dev/null
+++ b/charts/jenkins/templates/jenkins-controller-svc.yaml
@@ -0,0 +1,56 @@
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{template "jenkins.fullname" . }}
+  namespace: {{ template "jenkins.namespace" . }}
+  labels:
+    "app.kubernetes.io/name": '{{ template "jenkins.name" .}}'
+    {{- if .Values.renderHelmLabels }}
+    "helm.sh/chart": "{{ template "jenkins.label" .}}"
+    {{- end }}
+    "app.kubernetes.io/managed-by": "{{ .Release.Service }}"
+    "app.kubernetes.io/instance": "{{ .Release.Name }}"
+    "app.kubernetes.io/component": "{{ .Values.controller.componentName }}"
+    {{- if .Values.controller.serviceLabels }}
+{{ toYaml .Values.controller.serviceLabels | indent 4 }}
+    {{- end }}
+{{- if .Values.controller.serviceAnnotations }}
+  annotations:
+{{ toYaml .Values.controller.serviceAnnotations | indent 4 }}
+{{- end }}
+spec:
+  {{- if .Values.controller.serviceExternalTrafficPolicy }}
+  externalTrafficPolicy: {{.Values.controller.serviceExternalTrafficPolicy}}
+  {{- end }}
+  {{- if (and (eq .Values.controller.serviceType "ClusterIP") (not (empty .Values.controller.clusterIP))) }}
+  clusterIP: {{.Values.controller.clusterIP}}
+  {{- end }}
+  ports:
+    - port: {{.Values.controller.servicePort}}
+      name: http
+      targetPort: {{ .Values.controller.targetPort }}
+      {{- if (and (eq .Values.controller.serviceType "NodePort") (not (empty .Values.controller.nodePort))) }}
+      nodePort: {{.Values.controller.nodePort}}
+      {{- end }}
+{{- range $index, $port := .Values.controller.extraPorts }}
+    - port: {{ $port.port }}
+      name: {{ $port.name }}
+      {{- if $port.targetPort }}
+      targetPort: {{ $port.targetPort }}
+      {{- else }}
+      targetPort: {{ $port.port }}
+      {{- end -}}
+{{- end }}
+  selector:
+    "app.kubernetes.io/component": "{{ .Values.controller.componentName }}"
+    "app.kubernetes.io/instance": "{{ .Release.Name }}"
+  type: {{.Values.controller.serviceType}}
+  {{if eq .Values.controller.serviceType "LoadBalancer"}}
+{{- if .Values.controller.loadBalancerSourceRanges }}
+  loadBalancerSourceRanges:
+{{ toYaml .Values.controller.loadBalancerSourceRanges | indent 4 }}
+{{- end }}
+  {{if .Values.controller.loadBalancerIP}}
+  loadBalancerIP: {{.Values.controller.loadBalancerIP}}
+  {{end}}
+  {{end}}
diff --git a/charts/jenkins/templates/rbac.yaml b/charts/jenkins/templates/rbac.yaml
new file mode 100644
index 0000000..581cb8d
--- /dev/null
+++ b/charts/jenkins/templates/rbac.yaml
@@ -0,0 +1,149 @@
+{{ if .Values.rbac.create }}
+{{- $serviceName := include "jenkins.fullname" . -}}
+
+# This role is used to allow Jenkins scheduling of agents via Kubernetes plugin.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: {{ $serviceName }}-schedule-agents
+  namespace: {{ template "jenkins.agent.namespace" . }}
+  labels:
+    "app.kubernetes.io/name": '{{ template "jenkins.name" .}}'
+    {{- if .Values.renderHelmLabels }}
+    "helm.sh/chart": "{{ template "jenkins.label" .}}"
+    {{- end }}
+    "app.kubernetes.io/managed-by": "{{ .Release.Service }}"
+    "app.kubernetes.io/instance": "{{ .Release.Name }}"
+    "app.kubernetes.io/component": "{{ .Values.controller.componentName }}"
+rules:
+- apiGroups: [""]
+  resources: ["pods", "pods/exec", "pods/log", "persistentvolumeclaims", "events"]
+  verbs: ["get", "list", "watch"]
+- apiGroups: [""]
+  resources: ["pods", "pods/exec", "persistentvolumeclaims"]
+  verbs: ["create", "delete", "deletecollection", "patch", "update"]
+
+---
+
+# We bind the role to the Jenkins service account. The role binding is created in the namespace
+# where the agents are supposed to run.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  name: {{ $serviceName }}-schedule-agents
+  namespace: {{ template "jenkins.agent.namespace" . }}
+  labels:
+    "app.kubernetes.io/name": '{{ template "jenkins.name" .}}'
+    {{- if .Values.renderHelmLabels }}
+    "helm.sh/chart": "{{ template "jenkins.label" .}}"
+    {{- end }}
+    "app.kubernetes.io/managed-by": "{{ .Release.Service }}"
+    "app.kubernetes.io/instance": "{{ .Release.Name }}"
+    "app.kubernetes.io/component": "{{ .Values.controller.componentName }}"
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: {{ $serviceName }}-schedule-agents
+subjects:
+- kind: ServiceAccount
+  name: {{ template "jenkins.serviceAccountName" .}}
+  namespace: {{ template "jenkins.namespace" . }}
+
+---
+
+{{- if .Values.rbac.readSecrets }}
+# This is needed if you want to use https://jenkinsci.github.io/kubernetes-credentials-provider-plugin/
+# as it needs permissions to get/watch/list Secrets
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: {{ template "jenkins.fullname" . }}-read-secrets
+  namespace: {{ template "jenkins.namespace" . }}
+  labels:
+    "app.kubernetes.io/name": '{{ template "jenkins.name" .}}'
+    {{- if .Values.renderHelmLabels }}
+    "helm.sh/chart": "{{ template "jenkins.label" .}}"
+    {{- end }}
+    "app.kubernetes.io/managed-by": "{{ .Release.Service }}"
+    "app.kubernetes.io/instance": "{{ .Release.Name }}"
+    "app.kubernetes.io/component": "{{ .Values.controller.componentName }}"
+rules:
+  - apiGroups: [""]
+    resources: ["secrets"]
+    verbs: ["get", "watch", "list"]
+
+---
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  name: {{ $serviceName }}-read-secrets
+  namespace: {{ template "jenkins.namespace" . }}
+  labels:
+    "app.kubernetes.io/name": '{{ template "jenkins.name" .}}'
+    {{- if .Values.renderHelmLabels }}
+    "helm.sh/chart": "{{ template "jenkins.label" .}}"
+    {{- end }}
+    "app.kubernetes.io/managed-by": "{{ .Release.Service }}"
+    "app.kubernetes.io/instance": "{{ .Release.Name }}"
+    "app.kubernetes.io/component": "{{ .Values.controller.componentName }}"
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: {{ template "jenkins.fullname" . }}-read-secrets
+subjects:
+  - kind: ServiceAccount
+    name: {{ template "jenkins.serviceAccountName" . }}
+    namespace: {{ template "jenkins.namespace" . }}
+
+---
+{{- end}}
+
+{{- if .Values.controller.sidecars.configAutoReload.enabled }}
+# The sidecar container which is responsible for reloading configuration changes
+# needs permissions to watch ConfigMaps
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: {{ template "jenkins.fullname" . }}-casc-reload
+  namespace: {{ template "jenkins.namespace" . }}
+  labels:
+    "app.kubernetes.io/name": '{{ template "jenkins.name" .}}'
+    {{- if .Values.renderHelmLabels }}
+    "helm.sh/chart": "{{ template "jenkins.label" .}}"
+    {{- end }}
+    "app.kubernetes.io/managed-by": "{{ .Release.Service }}"
+    "app.kubernetes.io/instance": "{{ .Release.Name }}"
+    "app.kubernetes.io/component": "{{ .Values.controller.componentName }}"
+rules:
+- apiGroups: [""]
+  resources: ["configmaps"]
+  verbs: ["get", "watch", "list"]
+
+---
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  name: {{ $serviceName }}-watch-configmaps
+  namespace: {{ template "jenkins.namespace" . }}
+  labels:
+    "app.kubernetes.io/name": '{{ template "jenkins.name" .}}'
+    {{- if .Values.renderHelmLabels }}
+    "helm.sh/chart": "{{ template "jenkins.label" .}}"
+    {{- end }}
+    "app.kubernetes.io/managed-by": "{{ .Release.Service }}"
+    "app.kubernetes.io/instance": "{{ .Release.Name }}"
+    "app.kubernetes.io/component": "{{ .Values.controller.componentName }}"
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: {{ template "jenkins.fullname" . }}-casc-reload
+subjects:
+- kind: ServiceAccount
+  name: {{ template "jenkins.serviceAccountName" . }}
+  namespace: {{ template "jenkins.namespace" . }}
+
+{{- end}}
+
+{{ end }}
diff --git a/charts/jenkins/templates/secret-additional.yaml b/charts/jenkins/templates/secret-additional.yaml
new file mode 100644
index 0000000..d1908aa
--- /dev/null
+++ b/charts/jenkins/templates/secret-additional.yaml
@@ -0,0 +1,21 @@
+{{- if .Values.controller.additionalSecrets -}}
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: {{ template "jenkins.fullname" . }}-additional-secrets
+  namespace: {{ template "jenkins.namespace" . }}
+  labels:
+    "app.kubernetes.io/name": '{{ template "jenkins.name" .}}'
+    {{- if .Values.renderHelmLabels }}
+    "helm.sh/chart": "{{ template "jenkins.label" .}}"
+    {{- end }}
+    "app.kubernetes.io/managed-by": "{{ .Release.Service }}"
+    "app.kubernetes.io/instance": "{{ .Release.Name }}"
+    "app.kubernetes.io/component": "{{ .Values.controller.componentName }}"
+type: Opaque
+data:
+{{- range .Values.controller.additionalSecrets }}
+  {{ .name }}: {{ .value | b64enc }}
+{{- end }}
+{{- end }}
diff --git a/charts/jenkins/templates/secret-claims.yaml b/charts/jenkins/templates/secret-claims.yaml
new file mode 100644
index 0000000..e8b6d6c
--- /dev/null
+++ b/charts/jenkins/templates/secret-claims.yaml
@@ -0,0 +1,29 @@
+{{- if .Values.controller.secretClaims -}}
+{{- $r := .Release -}}
+{{- $v := .Values -}}
+{{- $chart := printf "%s-%s" .Chart.Name .Chart.Version -}}
+{{- $namespace := include "jenkins.namespace" . -}}
+{{- $serviceName := include "jenkins.fullname" . -}}
+{{ range .Values.controller.secretClaims }}
+---
+kind: SecretClaim
+apiVersion: vaultproject.io/v1
+metadata:
+  name: {{ $serviceName }}-{{ .name | default .path | lower }}
+  namespace: {{ $namespace }}
+  labels:
+    "app.kubernetes.io/name": '{{ $serviceName }}'
+    {{- if $v.renderHelmLabels }}
+    "helm.sh/chart": "{{ $chart }}"
+    {{- end }}
+    "app.kubernetes.io/managed-by": "{{ $r.Service }}"
+    "app.kubernetes.io/instance": "{{ $r.Name }}"
+    "app.kubernetes.io/component": "{{ $v.controller.componentName }}"
+spec:
+  type: {{ .type | default "Opaque" }}
+  path: {{ .path }}
+{{- if .renew }}
+  renew: {{ .renew }}
+{{- end }}
+{{- end }}
+{{- end }}
\ No newline at end of file
diff --git a/charts/jenkins/templates/secret-https-jks.yaml b/charts/jenkins/templates/secret-https-jks.yaml
new file mode 100644
index 0000000..5348de4
--- /dev/null
+++ b/charts/jenkins/templates/secret-https-jks.yaml
@@ -0,0 +1,20 @@
+{{- if and .Values.controller.httpsKeyStore.enable ( not  .Values.controller.httpsKeyStore.jenkinsHttpsJksSecretName ) (not .Values.controller.httpsKeyStore.disableSecretMount) -}}
+apiVersion: v1
+kind: Secret
+metadata:
+  name: {{ template "jenkins.fullname" . }}-https-jks
+  namespace: {{ template "jenkins.namespace" . }}
+  labels:
+    "app.kubernetes.io/name": '{{ template "jenkins.name" .}}'
+    {{- if .Values.renderHelmLabels }}
+    "helm.sh/chart": "{{ template "jenkins.label" .}}"
+    {{- end }}
+    "app.kubernetes.io/managed-by": "{{ .Release.Service }}"
+    "app.kubernetes.io/instance": "{{ .Release.Name }}"
+    "app.kubernetes.io/component": "{{ .Values.controller.componentName }}"
+type: Opaque
+data:
+  jenkins-jks-file: |
+{{ .Values.controller.httpsKeyStore.jenkinsKeyStoreBase64Encoded | indent 4 }}
+  https-jks-password: {{ .Values.controller.httpsKeyStore.password | b64enc }}
+{{- end }}
diff --git a/charts/jenkins/templates/secret.yaml b/charts/jenkins/templates/secret.yaml
new file mode 100644
index 0000000..cc6ace1
--- /dev/null
+++ b/charts/jenkins/templates/secret.yaml
@@ -0,0 +1,20 @@
+{{- if and (not .Values.controller.admin.existingSecret) (.Values.controller.admin.createSecret) -}}
+
+apiVersion: v1
+kind: Secret
+metadata:
+  name: {{ template "jenkins.fullname" . }}
+  namespace: {{ template "jenkins.namespace" . }}
+  labels:
+    "app.kubernetes.io/name": '{{ template "jenkins.name" .}}'
+    {{- if .Values.renderHelmLabels }}
+    "helm.sh/chart": "{{ template "jenkins.label" .}}"
+    {{- end }}
+    "app.kubernetes.io/managed-by": "{{ .Release.Service }}"
+    "app.kubernetes.io/instance": "{{ .Release.Name }}"
+    "app.kubernetes.io/component": "{{ .Values.controller.componentName }}"
+type: Opaque
+data:
+  jenkins-admin-password: {{ template "jenkins.password" . }}
+  jenkins-admin-user: {{ .Values.controller.admin.username | b64enc | quote }}
+{{- end }}
diff --git a/charts/jenkins/templates/service-account-agent.yaml b/charts/jenkins/templates/service-account-agent.yaml
new file mode 100644
index 0000000..48f08ba
--- /dev/null
+++ b/charts/jenkins/templates/service-account-agent.yaml
@@ -0,0 +1,26 @@
+{{ if .Values.serviceAccountAgent.create }}
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: {{ include "jenkins.serviceAccountAgentName" . }}
+  namespace: {{ template "jenkins.agent.namespace" . }}
+{{- if .Values.serviceAccountAgent.annotations }}
+  annotations:
+{{ tpl (toYaml .Values.serviceAccountAgent.annotations) . | indent 4 }}
+{{- end }}
+  labels:
+    "app.kubernetes.io/name": '{{ template "jenkins.name" .}}'
+    {{- if .Values.renderHelmLabels }}
+    "helm.sh/chart": "{{ template "jenkins.label" .}}"
+    {{- end }}
+    "app.kubernetes.io/managed-by": "{{ .Release.Service }}"
+    "app.kubernetes.io/instance": "{{ .Release.Name }}"
+    "app.kubernetes.io/component": "{{ .Values.controller.componentName }}"
+{{- if .Values.serviceAccountAgent.extraLabels }}
+{{ tpl (toYaml .Values.serviceAccountAgent.extraLabels) . | indent 4 }}
+{{- end }}
+{{- if .Values.serviceAccountAgent.imagePullSecretName }}
+imagePullSecrets:
+  - name: {{ .Values.serviceAccountAgent.imagePullSecretName }}
+{{- end -}}
+{{ end }}
diff --git a/charts/jenkins/templates/service-account.yaml b/charts/jenkins/templates/service-account.yaml
new file mode 100644
index 0000000..b44eb48
--- /dev/null
+++ b/charts/jenkins/templates/service-account.yaml
@@ -0,0 +1,26 @@
+{{ if .Values.serviceAccount.create }}
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: {{ include "jenkins.serviceAccountName" . }}
+  namespace: {{ template "jenkins.namespace" . }}
+{{- if .Values.serviceAccount.annotations }}
+  annotations:
+{{ tpl (toYaml .Values.serviceAccount.annotations) . | indent 4 }}
+{{- end }}
+  labels:
+    "app.kubernetes.io/name": '{{ template "jenkins.name" .}}'
+    {{- if .Values.renderHelmLabels }}
+    "helm.sh/chart": "{{ template "jenkins.label" .}}"
+    {{- end }}
+    "app.kubernetes.io/managed-by": "{{ .Release.Service }}"
+    "app.kubernetes.io/instance": "{{ .Release.Name }}"
+    "app.kubernetes.io/component": "{{ .Values.controller.componentName }}"
+{{- if .Values.serviceAccount.extraLabels }}
+{{ tpl (toYaml .Values.serviceAccount.extraLabels) . | indent 4 }}
+{{- end }}
+{{- if .Values.serviceAccount.imagePullSecretName }}
+imagePullSecrets:
+  - name: {{ .Values.serviceAccount.imagePullSecretName }}
+{{- end -}}
+{{ end }}
diff --git a/charts/jenkins/templates/tests/jenkins-test.yaml b/charts/jenkins/templates/tests/jenkins-test.yaml
new file mode 100644
index 0000000..12a935e
--- /dev/null
+++ b/charts/jenkins/templates/tests/jenkins-test.yaml
@@ -0,0 +1,49 @@
+{{- if .Values.controller.testEnabled }}
+apiVersion: v1
+kind: Pod
+metadata:
+  name: "{{ .Release.Name }}-ui-test-{{ randAlphaNum 5 | lower }}"
+  namespace: {{ template "jenkins.namespace" . }}
+  annotations:
+    "helm.sh/hook": test-success
+spec:
+  {{- if .Values.controller.nodeSelector }}
+  nodeSelector:
+{{ toYaml .Values.controller.nodeSelector | indent 4 }}
+  {{- end }}
+  {{- if .Values.controller.tolerations }}
+  tolerations:
+{{ toYaml .Values.controller.tolerations | indent 4 }}
+  {{- end }}
+  initContainers:
+    - name: "test-framework"
+      image: "{{ .Values.helmtest.bats.image.registry }}/{{ .Values.helmtest.bats.image.repository }}:{{ .Values.helmtest.bats.image.tag }}"
+      command:
+        - "bash"
+        - "-c"
+      args:
+        - |
+          # copy bats to tools dir
+          set -ex
+          cp -R /opt/bats /tools/bats/
+      volumeMounts:
+      - mountPath: /tools
+        name: tools
+  containers:
+    - name: {{ .Release.Name }}-ui-test
+      image: "{{ .Values.controller.image.registry }}/{{ .Values.controller.image.repository }}:{{- include "controller.image.tag" . -}}"
+      command: ["/tools/bats/bin/bats", "-t", "/tests/run.sh"]
+      volumeMounts:
+      - mountPath: /tests
+        name: tests
+        readOnly: true
+      - mountPath: /tools
+        name: tools
+  volumes:
+  - name: tests
+    configMap:
+      name: {{ template "jenkins.fullname" . }}-tests
+  - name: tools
+    emptyDir: {}
+  restartPolicy: Never
+{{- end }}
diff --git a/charts/jenkins/templates/tests/test-config.yaml b/charts/jenkins/templates/tests/test-config.yaml
new file mode 100644
index 0000000..12c5b3a
--- /dev/null
+++ b/charts/jenkins/templates/tests/test-config.yaml
@@ -0,0 +1,14 @@
+{{- if .Values.controller.testEnabled }}
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: {{ template "jenkins.fullname" . }}-tests
+  namespace: {{ template "jenkins.namespace" . }}
+  annotations:
+    "helm.sh/hook": test
+data:
+  run.sh: |-
+    @test "Testing Jenkins UI is accessible" {
+      curl --retry 48 --retry-delay 10 {{ template "jenkins.fullname" . }}:{{ .Values.controller.servicePort }}{{ default "" .Values.controller.jenkinsUriPrefix }}/login
+    }
+{{- end }}