update charts
diff --git a/charts/k8s-gerrit/.github/PULL_REQUEST_TEMPLATE.md b/charts/k8s-gerrit/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..fd8a1af
--- /dev/null
+++ b/charts/k8s-gerrit/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,9 @@
+# Important Notice
+
+Patch submission and review is done through
+[Gerrit Code Review](https://gerrit-review.googlesource.com).
+Unfortunately we cannot pull your code as a Pull Request.
+
+__NO REVIEWS OR DISCUSSIONS will happen on GitHub__, all the
+[code collaboration](../Documentation/developer-guide.md)
+will take place on Gerrit.
diff --git a/charts/k8s-gerrit/.gitignore b/charts/k8s-gerrit/.gitignore
new file mode 100644
index 0000000..6beacc0
--- /dev/null
+++ b/charts/k8s-gerrit/.gitignore
@@ -0,0 +1,14 @@
+*.pem
+*.crt
+/helm-charts/*/charts
+/helm-charts/*/requirements.lock
+
+__pycache__
+.pytest_cache
+*.pyc
+
+*bin/
+.DS_Store
+.vscode/
+
+.project
\ No newline at end of file
diff --git a/charts/k8s-gerrit/.mailmap b/charts/k8s-gerrit/.mailmap
new file mode 100644
index 0000000..cc103a4
--- /dev/null
+++ b/charts/k8s-gerrit/.mailmap
@@ -0,0 +1,2 @@
+Matthias Sohn <matthias.sohn@sap.com> <matthias.sohn@gmail.com>
+Thomas Draebing <thomas.draebing@sap.com>
diff --git a/charts/k8s-gerrit/.pylintrc b/charts/k8s-gerrit/.pylintrc
new file mode 100644
index 0000000..2e74428
--- /dev/null
+++ b/charts/k8s-gerrit/.pylintrc
@@ -0,0 +1,15 @@
+[MESSAGES CONTROL]
+disable=C0111, W0621, R0201, R0913, R0903, W0511, C0330
+
+[BASIC]
+no-docstring-rgx=(test_.*)|(__.*__)
+
+[FORMAT]
+indent-string='    '
+good-names=i,f
+
+[SIMILARITIES]
+min-similarity-lines=6
+
+[MASTER]
+init-hook='import sys; sys.path.append("./tests/helpers")'
diff --git a/charts/k8s-gerrit/Documentation/developer-guide.md b/charts/k8s-gerrit/Documentation/developer-guide.md
new file mode 100644
index 0000000..0e5fc5f
--- /dev/null
+++ b/charts/k8s-gerrit/Documentation/developer-guide.md
@@ -0,0 +1,83 @@
+# Developer Guide
+
+[TOC]
+
+## Code Review
+
+This project uses Gerrit for code review:
+https://gerrit-review.googlesource.com/
+which uses the ["git push" workflow][1] with server
+https://gerrit.googlesource.com/k8s-gerrit. You will need a
+[generated cookie][2].
+
+Gerrit depends on "Change-Id" annotations in your commit message.
+If you try to push a commit without one, it will explain how to
+install the proper git-hook:
+
+```
+curl -Lo `git rev-parse --git-dir`/hooks/commit-msg \
+    https://gerrit-review.googlesource.com/tools/hooks/commit-msg
+chmod +x `git rev-parse --git-dir`/hooks/commit-msg
+```
+
+Before you create your local commit (which you'll push to Gerrit)
+you will need to set your email to match your Gerrit account:
+
+```
+git config --local --add user.email foo@bar.com
+```
+
+Normally you will create code reviews by pushing for master:
+
+```
+git push origin HEAD:refs/for/master
+```
+
+## Developing container images
+
+When changing or creating container images, keep the image size as small as
+possible. This reduces storage space needed for images, the upload time and most
+importantly the download time, which improves startup time of pods.
+
+Some good practices are listed here:
+
+- **Chain commands:** Each `RUN`-command creates a new layer in the docker image.
+Each layer increases the total image size. Thus, reducing the number of layers,
+can also reduce the image size.
+
+- **Clean up after package installation:** The package installation creates a
+number of cache files, which should be removed after installation. In Ubuntu/Debian-
+based images use the following snippet (This requires `apt-get update` before
+each package installation!):
+
+```docker
+RUN apt-get update && \
+    apt get install -y <packages> && \
+    apt-get clean && \
+    rm -rf /var/lib/apt/lists/*
+```
+
+In Alpine based images use the `--no-cache`-flag of `apk`.
+
+- **Clean up temporary files immediately:** If temporary files are created by a
+command remove them in the same command chain.
+
+- **Use multi stage builds:** If some complicated build processes are needed for
+building parts of the container image, of which only the final product is needed,
+use [multi stage builds][3]
+
+
+[1]: https://gerrit-review.googlesource.com/Documentation/user-upload.html#_git_push
+[2]: https://gerrit.googlesource.com/new-password
+[3]: https://docs.docker.com/develop/develop-images/multistage-build/
+
+## Writing clean python code
+
+When writing python code, either for tests or for scripts, use `black` and `pylint`
+to ensure a clean code style. They can be run by the following commands:
+
+```sh
+pipenv install --dev
+pipenv run black $(find . -name '*.py')
+pipenv run pylint $(find . -name '*.py')
+```
diff --git a/charts/k8s-gerrit/Documentation/istio.md b/charts/k8s-gerrit/Documentation/istio.md
new file mode 100644
index 0000000..1803b32
--- /dev/null
+++ b/charts/k8s-gerrit/Documentation/istio.md
@@ -0,0 +1,40 @@
+# Istio
+
+Istio provides an alternative way to control ingress traffic into the cluster.
+In addition, it allows to finetune the traffic inside the cluster and provides
+a huge repertoire of load balancing and routing mechanisms.
+
+***note
+Currently, only the Gerrit replica chart allows using istio out of the box.
+***
+
+## Install istio
+
+An example configuration based on the default profile provided by istio can be
+found under `./istio/src/`. Some values will have to be adapted to the respective
+system. These are marked by comments tagged with `TO_BE_CHANGED`.
+To install istio with this configuration, run:
+
+```sh
+kubectl apply -f istio/istio-system-namespace.yaml
+istioctl install -f istio/gerrit.profile.yaml
+```
+
+To install Gerrit using istio for networking, the namespace running Gerrit has to
+be configured to enable sidecar injection, by setting the `istio-injection: enabled`
+label. An example for such a namespace can be found at `./istio/namespace.yaml`.
+
+## Uninstall istio
+
+To uninstall istio, run:
+
+```sh
+istioctl uninstall -f istio/gerrit.profile.yaml
+```
+
+## Restricting access to a list of allowed IPs
+
+In development setups, it might be wanted to allow access to the setup only from
+specified IPs. This can be done by patching the `spec.loadBalancerSourceRanges`
+value of the service used for the IngressGateway. A patch doing that can be
+uncommented in `istio/gerrit.profile.yaml`.
diff --git a/charts/k8s-gerrit/Documentation/minikube.md b/charts/k8s-gerrit/Documentation/minikube.md
new file mode 100644
index 0000000..18af7cc
--- /dev/null
+++ b/charts/k8s-gerrit/Documentation/minikube.md
@@ -0,0 +1,207 @@
+# Running Gerrit on Kubernetes using Minikube
+
+To test Gerrit on Kubernetes locally, a one-node cluster can be set up using
+Minikube. Minikube provides basic Kubernetes functionality and allows to quickly
+deploy and evaluate a Kubernetes deployment.
+This tutorial will guide through setting up Minikube to deploy the gerrit and
+gerrit-replica helm charts to it. Note, that due to limited compute
+resources on a single local machine and the restricted functionality of Minikube,
+the full functionality of the charts might not be usable.
+
+## Installing Kubectl and Minikube
+
+To use Minikube, a hypervisor is needed. A good non-commercial solution is HyperKit.
+The Minikube project provides binaries to install the driver:
+
+```sh
+curl -LO https://storage.googleapis.com/minikube/releases/latest/docker-machine-driver-hyperkit \
+  && sudo install -o root -g wheel -m 4755 docker-machine-driver-hyperkit /usr/local/bin/
+```
+
+To manage Kubernetes clusters, the Kubectl CLI tool will be needed. A detailed
+guide how to do that for all supported OSs can be found
+[here](https://kubernetes.io/docs/tasks/tools/install-kubectl/#install-with-homebrew-on-macos).
+On OSX hombrew can be used for installation:
+
+```sh
+brew install kubernetes-cli
+```
+
+Finally, Minikube can be installed. Download the latest binary
+[here](https://github.com/kubernetes/minikube/releases). To install it on OSX, run:
+
+```sh
+VERSION=1.1.0
+curl -Lo minikube https://storage.googleapis.com/minikube/releases/v$VERSION/minikube-darwin-amd64 && \
+  chmod +x minikube && \
+  sudo cp minikube /usr/local/bin/ && \
+  rm minikube
+```
+
+## Starting a Minikube cluster
+
+For a more detailed overview over the features of Minikube refer to the
+[official documentation](https://kubernetes.io/docs/setup/minikube/). If a
+hypervisor driver other than virtual box (e.g. hyperkit) is used, set the
+`--vm-driver` option accordingly:
+
+```sh
+minikube config set vm-driver hyperkit
+```
+
+The gerrit and gerrit-replica charts are configured to work with the default
+resource limits configured for minikube (2 cpus and 2Gi RAM). If more resources
+are desired (e.g. to speed up deployment startup or for more resource intensive
+tests), configure the resource limits using:
+
+```sh
+minikube config set memory 4096
+minikube config set cpus 4
+```
+
+To install a full Gerrit and Gerrit replica setup with reasonable startup
+times, Minikube will need about 9.5 GB of RAM and 3-4 CPUs! But the more the
+better.
+
+To start a Minikube cluster simply run:
+
+```sh
+minikube start
+```
+
+Starting up the cluster will take a while. The installation should automatically
+configure kubectl to connect to the Minikube cluster. Run the following command
+to test whether the cluster is up:
+
+```sh
+kubectl get nodes
+
+NAME       STATUS   ROLES    AGE   VERSION
+minikube   Ready    master   1h    v1.14.2
+```
+
+The helm-charts use ingresses, which can be used in Minikube by enabling the
+ingress addon:
+
+```sh
+minikube addons enable ingress
+```
+
+Since for testing there will probably no usable host names configured to point
+to the minikube installation, the traffic to the hostnames configured in the
+Ingress definition needs to be redirected to Minikube by editing the `/etc/hosts`-
+file, adding a line containing the Minikube IP and a whitespace-delimited list
+of all the hostnames:
+
+```sh
+echo "$(minikube ip) primary.gerrit backend.gerrit replica.gerrit" | sudo tee -a /etc/hosts
+```
+
+The host names (e.g. `primary.gerrit`) are the defaults, when using the values.yaml
+files provided as and example for minikube. Change them accordingly, if a different
+one is chosen.
+This will only redirect traffic from the computer running Minikube.
+
+To see whether all cluster components are ready, run:
+
+```sh
+kubectl get pods --all-namespaces
+```
+
+The status of all components should be `Ready`. The kubernetes dashboard giving
+an overview over all cluster components, can be opened by executing:
+
+```sh
+minikube dashboard
+```
+
+## Install helm
+
+Helm is needed to install and manage the helm charts. To install the helm client
+on your local machine (running OSX), run:
+
+```sh
+brew install kubernetes-helm
+```
+
+A guide for all suported OSs can be found [here](https://docs.helm.sh/using_helm/#installing-helm).
+
+## Start an NFS-server
+
+The helm-charts need a volume with ReadWriteMany access mode to store
+git-repositories. This guide will use the nfs-server-provisioner chart to provide
+NFS-volumes directly in the cluster. A basic configuration file for the nfs-server-
+provisioner-chart is provided in the supplements-directory. It can be installed
+by running:
+
+```sh
+helm install nfs \
+  stable/nfs-server-provisioner \
+  -f ./supplements/nfs.minikube.values.yaml
+```
+
+## Installing the gerrit helm chart
+
+A configuration file to configure the gerrit chart is provided at
+`./supplements/gerrit.minikube.values.yaml`. To install the gerrit
+chart on Minikube, run:
+
+```sh
+helm install gerrit \
+  ./helm-charts/gerrit \
+  -f ./supplements/gerrit.minikube.values.yaml
+```
+
+Startup may take some time, especially when allowing only a small amount of
+resources to the containers. Check progress with `kubectl get pods -w` until
+it says that the pod `gerrit-gerrit-stateful-set-0` is `Running`.
+Then use `kubectl logs -f gerrit-gerrit-stateful-set-0` to follow
+the startup process of Gerrit until a line like this shows that Gerrit is ready:
+
+```sh
+[2019-06-04 15:24:25,914] [main] INFO  com.google.gerrit.pgm.Daemon : Gerrit Code Review 2.16.8-86-ga831ebe687 ready
+```
+
+To open Gerrit's UI, run:
+
+```sh
+open http://primary.gerrit
+```
+
+## Installing the gerrit-replica helm chart
+
+A custom configuration file to configure the gerrit-replica chart is provided at
+`./supplements/gerrit-replica.minikube.values.yaml`. Install it by running:
+
+```sh
+helm install gerrit-replica \
+  ./helm-charts/gerrit-replica \
+  -f ./supplements/gerrit-replica.minikube.values.yaml
+```
+
+The replica will start up, which can be followed by running:
+
+```sh
+kubectl logs -f gerrit-replica-gerrit-replica-deployment-<id>
+```
+
+Replication of repositories has to be started on the Gerrit, e.g. by making
+a change in the respective repositories. Only then previous changes to the
+repositories will be available on the replica.
+
+## Cleanup
+
+Shut down minikube:
+
+```sh
+minikube stop
+```
+
+Delete the minikube cluster:
+
+```sh
+minikube delete
+```
+
+Remove the line added to `/etc/hosts`. If Minikube is restarted, the cluster will
+get a new IP and the `/etc/hosts`-entry has to be adjusted.
diff --git a/charts/k8s-gerrit/Documentation/operator-api-reference.md b/charts/k8s-gerrit/Documentation/operator-api-reference.md
new file mode 100644
index 0000000..5d03cf5
--- /dev/null
+++ b/charts/k8s-gerrit/Documentation/operator-api-reference.md
@@ -0,0 +1,1160 @@
+# Gerrit Operator - API Reference
+
+- [Gerrit Operator - API Reference](#gerrit-operator---api-reference)
+  - [General Remarks](#general-remarks)
+    - [Inheritance](#inheritance)
+  - [GerritCluster](#gerritcluster)
+  - [Gerrit](#gerrit)
+  - [Receiver](#receiver)
+  - [GitGarbageCollection](#gitgarbagecollection)
+  - [GerritNetwork](#gerritnetwork)
+  - [GerritClusterSpec](#gerritclusterspec)
+  - [GerritClusterStatus](#gerritclusterstatus)
+  - [StorageConfig](#storageconfig)
+  - [GerritStorageConfig](#gerritstorageconfig)
+  - [StorageClassConfig](#storageclassconfig)
+  - [NfsWorkaroundConfig](#nfsworkaroundconfig)
+  - [SharedStorage](#sharedstorage)
+  - [PluginCacheConfig](#plugincacheconfig)
+  - [ExternalPVCConfig](#externalpvcconfig)
+  - [ContainerImageConfig](#containerimageconfig)
+  - [BusyBoxImage](#busyboximage)
+  - [GerritRepositoryConfig](#gerritrepositoryconfig)
+  - [GerritClusterIngressConfig](#gerritclusteringressconfig)
+  - [GerritIngressTlsConfig](#gerritingresstlsconfig)
+  - [GerritIngressAmbassadorConfig](#gerritingressambassadorconfig)
+  - [GlobalRefDbConfig](#globalrefdbconfig)
+  - [RefDatabase](#refdatabase)
+  - [SpannerRefDbConfig](#spannerrefdbconfig)
+  - [ZookeeperRefDbConfig](#zookeeperrefdbconfig)
+  - [GerritTemplate](#gerrittemplate)
+  - [GerritTemplateSpec](#gerrittemplatespec)
+  - [GerritProbe](#gerritprobe)
+  - [GerritServiceConfig](#gerritserviceconfig)
+  - [GerritSite](#gerritsite)
+  - [GerritModule](#gerritmodule)
+  - [GerritPlugin](#gerritplugin)
+  - [GerritMode](#gerritmode)
+  - [GerritDebugConfig](#gerritdebugconfig)
+  - [GerritSpec](#gerritspec)
+  - [GerritStatus](#gerritstatus)
+  - [IngressConfig](#ingressconfig)
+  - [ReceiverTemplate](#receivertemplate)
+  - [ReceiverTemplateSpec](#receivertemplatespec)
+  - [ReceiverSpec](#receiverspec)
+  - [ReceiverStatus](#receiverstatus)
+  - [ReceiverProbe](#receiverprobe)
+  - [ReceiverServiceConfig](#receiverserviceconfig)
+  - [GitGarbageCollectionSpec](#gitgarbagecollectionspec)
+  - [GitGarbageCollectionStatus](#gitgarbagecollectionstatus)
+  - [GitGcState](#gitgcstate)
+  - [GerritNetworkSpec](#gerritnetworkspec)
+  - [NetworkMember](#networkmember)
+  - [NetworkMemberWithSsh](#networkmemberwithssh)
+
+## General Remarks
+
+### Inheritance
+
+Some objects inherit the fields of other objects. In this case the section will
+contain an **Extends:** label to link to the parent object, but it will not repeat
+inherited fields.
+
+## GerritCluster
+
+---
+
+**Group**: gerritoperator.google.com \
+**Version**: v1alpha17 \
+**Kind**: GerritCluster
+
+---
+
+
+| Field | Type | Description |
+|---|---|---|
+| `apiVersion` | `String` | APIVersion of this resource |
+| `kind` | `String` | Kind of this resource |
+| `metadata` | [`ObjectMeta`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#objectmeta-v1-meta) | Metadata of the resource |
+| `spec` | [`GerritClusterSpec`](#gerritclusterspec) | Specification for GerritCluster |
+| `status` | [`GerritClusterStatus`](#gerritclusterstatus) | Status for GerritCluster |
+
+Example:
+
+```yaml
+apiVersion: "gerritoperator.google.com/v1alpha17"
+kind: GerritCluster
+metadata:
+  name: gerrit
+spec:
+  containerImages:
+    imagePullSecrets: []
+    imagePullPolicy: Always
+    gerritImages:
+      registry: docker.io
+      org: k8sgerrit
+      tag: latest
+    busyBox:
+      registry: docker.io
+      tag: latest
+
+  storage:
+    storageClasses:
+      readWriteOnce: default
+      readWriteMany: shared-storage
+      nfsWorkaround:
+        enabled: false
+        chownOnStartup: false
+        idmapdConfig: |-
+          [General]
+            Verbosity = 0
+            Domain = localdomain.com
+
+          [Mapping]
+            Nobody-User = nobody
+            Nobody-Group = nogroup
+
+    sharedStorage:
+      externalPVC:
+        enabled: false
+        claimName: ""
+      size: 1Gi
+      volumeName: ""
+      selector:
+        matchLabels:
+          volume-type: ssd
+          aws-availability-zone: us-east-1
+
+    pluginCache:
+      enabled: false
+
+  ingress:
+    enabled: true
+    host: example.com
+    annotations: {}
+    tls:
+      enabled: false
+      secret: ""
+    ambassador:
+      id: []
+      createHost: false
+
+  refdb:
+    database: NONE
+    spanner:
+      projectName: ""
+      instance: ""
+      database: ""
+    zookeeper:
+      connectString: ""
+      rootNode: ""
+
+  serverId: ""
+
+  gerrits:
+  - metadata:
+      name: gerrit
+      labels:
+        app: gerrit
+    spec:
+      serviceAccount: gerrit
+
+      tolerations:
+      - key: key1
+        operator: Equal
+        value: value1
+        effect: NoSchedule
+
+      affinity:
+        nodeAffinity:
+        requiredDuringSchedulingIgnoredDuringExecution:
+          nodeSelectorTerms:
+          - matchExpressions:
+            - key: disktype
+              operator: In
+              values:
+              - ssd
+
+      topologySpreadConstraints: []
+      - maxSkew: 1
+        topologyKey: zone
+        whenUnsatisfiable: DoNotSchedule
+        labelSelector:
+          matchLabels:
+            foo: bar
+
+      priorityClassName: ""
+
+      replicas: 1
+      updatePartition: 0
+
+      resources:
+        requests:
+          cpu: 1
+          memory: 5Gi
+        limits:
+          cpu: 1
+          memory: 6Gi
+
+      startupProbe:
+        initialDelaySeconds: 0
+        periodSeconds: 10
+        timeoutSeconds: 1
+        successThreshold: 1
+        failureThreshold: 3
+
+      readinessProbe:
+        initialDelaySeconds: 0
+        periodSeconds: 10
+        timeoutSeconds: 1
+        successThreshold: 1
+        failureThreshold: 3
+
+      livenessProbe:
+        initialDelaySeconds: 0
+        periodSeconds: 10
+        timeoutSeconds: 1
+        successThreshold: 1
+        failureThreshold: 3
+
+      gracefulStopTimeout: 30
+
+      service:
+        type: NodePort
+        httpPort: 80
+        sshPort: 29418
+
+      mode: REPLICA
+
+      debug:
+        enabled: false
+        suspend: false
+
+      site:
+        size: 1Gi
+
+      plugins:
+      # Installs a packaged plugin
+      - name: delete-project
+
+      # Downloads and installs a plugin
+      - name: javamelody
+        url: https://gerrit-ci.gerritforge.com/view/Plugins-stable-3.6/job/plugin-javamelody-bazel-master-stable-3.6/lastSuccessfulBuild/artifact/bazel-bin/plugins/javamelody/javamelody.jar
+        sha1: 40ffcd00263171e373a24eb6a311791b2924707c
+
+      # If the `installAsLibrary` option is set to `true` the plugin's jar-file will
+      # be symlinked to the lib directory and thus installed as a library as well.
+      - name: saml
+        url: https://gerrit-ci.gerritforge.com/view/Plugins-stable-3.6/job/plugin-saml-bazel-master-stable-3.6/lastSuccessfulBuild/artifact/bazel-bin/plugins/saml/saml.jar
+        sha1: 6dfe8292d46b179638586e6acf671206f4e0a88b
+        installAsLibrary: true
+
+      libs:
+      - name: global-refdb
+        url: https://example.com/global-refdb.jar
+        sha1: 3d533a536b0d4e0184f824478c24bc8dfe896d06
+
+      configFiles:
+        gerrit.config: |-
+            [gerrit]
+              serverId = gerrit-1
+              disableReverseDnsLookup = true
+            [index]
+              type = LUCENE
+            [auth]
+              type = DEVELOPMENT_BECOME_ANY_ACCOUNT
+            [httpd]
+              requestLog = true
+              gracefulStopTimeout = 1m
+            [transfer]
+              timeout = 120 s
+            [user]
+              name = Gerrit Code Review
+              email = gerrit@example.com
+              anonymousCoward = Unnamed User
+            [container]
+              javaOptions = -Xms200m
+              javaOptions = -Xmx4g
+
+      secretRef: gerrit-secure-config
+
+  receiver:
+    metadata:
+      name: receiver
+      labels:
+        app: receiver
+    spec:
+      tolerations:
+      - key: key1
+        operator: Equal
+        value: value2
+        effect: NoSchedule
+
+      affinity:
+        nodeAffinity:
+        requiredDuringSchedulingIgnoredDuringExecution:
+          nodeSelectorTerms:
+          - matchExpressions:
+            - key: disktype
+              operator: In
+              values:
+              - ssd
+
+      topologySpreadConstraints: []
+      - maxSkew: 1
+        topologyKey: zone
+        whenUnsatisfiable: DoNotSchedule
+        labelSelector:
+          matchLabels:
+            foo: bar
+
+      priorityClassName: ""
+
+      replicas: 2
+      maxSurge: 1
+      maxUnavailable: 1
+
+      resources:
+        requests:
+          cpu: 1
+          memory: 5Gi
+        limits:
+          cpu: 1
+          memory: 6Gi
+
+      readinessProbe:
+        initialDelaySeconds: 0
+        periodSeconds: 10
+        timeoutSeconds: 1
+        successThreshold: 1
+        failureThreshold: 3
+
+      livenessProbe:
+        initialDelaySeconds: 0
+        periodSeconds: 10
+        timeoutSeconds: 1
+        successThreshold: 1
+        failureThreshold: 3
+
+      service:
+        type: NodePort
+        httpPort: 80
+
+      credentialSecretRef: receiver-credentials
+```
+
+## Gerrit
+
+---
+
+**Group**: gerritoperator.google.com \
+**Version**: v1alpha17 \
+**Kind**: Gerrit
+
+---
+
+
+| Field | Type | Description |
+|---|---|---|
+| `apiVersion` | `String` | APIVersion of this resource |
+| `kind` | `String` | Kind of this resource |
+| `metadata` | [`ObjectMeta`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#objectmeta-v1-meta) | Metadata of the resource |
+| `spec` | [`GerritSpec`](#gerritspec) | Specification for Gerrit |
+| `status` | [`GerritStatus`](#gerritstatus) | Status for Gerrit |
+
+Example:
+
+```yaml
+apiVersion: "gerritoperator.google.com/v1alpha17"
+kind: Gerrit
+metadata:
+  name: gerrit
+spec:
+  serviceAccount: gerrit
+
+  tolerations:
+    - key: key1
+      operator: Equal
+      value: value1
+      effect: NoSchedule
+
+  affinity:
+    nodeAffinity:
+      requiredDuringSchedulingIgnoredDuringExecution:
+        nodeSelectorTerms:
+        - matchExpressions:
+          - key: disktype
+            operator: In
+            values:
+            - ssd
+
+  topologySpreadConstraints:
+  - maxSkew: 1
+    topologyKey: zone
+    whenUnsatisfiable: DoNotSchedule
+    labelSelector:
+      matchLabels:
+        foo: bar
+
+  priorityClassName: ""
+
+  replicas: 1
+  updatePartition: 0
+
+  resources:
+    requests:
+      cpu: 1
+      memory: 5Gi
+    limits:
+      cpu: 1
+      memory: 6Gi
+
+  startupProbe:
+    initialDelaySeconds: 0
+    periodSeconds: 10
+    timeoutSeconds: 1
+    successThreshold: 1
+    failureThreshold: 3
+
+  readinessProbe:
+    initialDelaySeconds: 0
+    periodSeconds: 10
+    timeoutSeconds: 1
+    successThreshold: 1
+    failureThreshold: 3
+
+  livenessProbe:
+    initialDelaySeconds: 0
+    periodSeconds: 10
+    timeoutSeconds: 1
+    successThreshold: 1
+    failureThreshold: 3
+
+  gracefulStopTimeout: 30
+
+  service:
+    type: NodePort
+    httpPort: 80
+    sshPort: 29418
+
+  mode: PRIMARY
+
+  debug:
+    enabled: false
+    suspend: false
+
+  site:
+    size: 1Gi
+
+  plugins:
+  # Installs a plugin packaged into the gerrit.war file
+  - name: delete-project
+
+  # Downloads and installs a plugin
+  - name: javamelody
+    url: https://gerrit-ci.gerritforge.com/view/Plugins-stable-3.6/job/plugin-javamelody-bazel-master-stable-3.6/lastSuccessfulBuild/artifact/bazel-bin/plugins/javamelody/javamelody.jar
+    sha1: 40ffcd00263171e373a24eb6a311791b2924707c
+
+  # If the `installAsLibrary` option is set to `true` the plugin jar-file will
+  # be symlinked to the lib directory and thus installed as a library as well.
+  - name: saml
+    url: https://gerrit-ci.gerritforge.com/view/Plugins-stable-3.6/job/plugin-saml-bazel-master-stable-3.6/lastSuccessfulBuild/artifact/bazel-bin/plugins/saml/saml.jar
+    sha1: 6dfe8292d46b179638586e6acf671206f4e0a88b
+    installAsLibrary: true
+
+  libs:
+  - name: global-refdb
+    url: https://example.com/global-refdb.jar
+    sha1: 3d533a536b0d4e0184f824478c24bc8dfe896d06
+
+  configFiles:
+    gerrit.config: |-
+        [gerrit]
+          serverId = gerrit-1
+          disableReverseDnsLookup = true
+        [index]
+          type = LUCENE
+        [auth]
+          type = DEVELOPMENT_BECOME_ANY_ACCOUNT
+        [httpd]
+          requestLog = true
+          gracefulStopTimeout = 1m
+        [transfer]
+          timeout = 120 s
+        [user]
+          name = Gerrit Code Review
+          email = gerrit@example.com
+          anonymousCoward = Unnamed User
+        [container]
+          javaOptions = -Xms200m
+          javaOptions = -Xmx4g
+
+  secretRef: gerrit-secure-config
+
+  serverId: ""
+
+  containerImages:
+    imagePullSecrets: []
+    imagePullPolicy: Always
+    gerritImages:
+      registry: docker.io
+      org: k8sgerrit
+      tag: latest
+    busyBox:
+      registry: docker.io
+      tag: latest
+
+  storage:
+    storageClasses:
+      readWriteOnce: default
+      readWriteMany: shared-storage
+      nfsWorkaround:
+        enabled: false
+        chownOnStartup: false
+        idmapdConfig: |-
+          [General]
+            Verbosity = 0
+            Domain = localdomain.com
+
+          [Mapping]
+            Nobody-User = nobody
+            Nobody-Group = nogroup
+
+    sharedStorage:
+      externalPVC:
+        enabled: false
+        claimName: ""
+      size: 1Gi
+      volumeName: ""
+      selector:
+        matchLabels:
+          volume-type: ssd
+          aws-availability-zone: us-east-1
+
+    pluginCache:
+      enabled: false
+
+  ingress:
+    host: example.com
+    tlsEnabled: false
+
+  refdb:
+    database: NONE
+    spanner:
+      projectName: ""
+      instance: ""
+      database: ""
+    zookeeper:
+      connectString: ""
+      rootNode: ""
+```
+
+## Receiver
+
+---
+
+**Group**: gerritoperator.google.com \
+**Version**: v1alpha6 \
+**Kind**: Receiver
+
+---
+
+
+| Field | Type | Description |
+|---|---|---|
+| `apiVersion` | `String` | APIVersion of this resource |
+| `kind` | `String` | Kind of this resource |
+| `metadata` | [`ObjectMeta`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#objectmeta-v1-meta) | Metadata of the resource |
+| `spec` | [`ReceiverSpec`](#receiverspec) | Specification for Receiver |
+| `status` | [`ReceiverStatus`](#receiverstatus) | Status for Receiver |
+
+Example:
+
+```yaml
+apiVersion: "gerritoperator.google.com/v1alpha6"
+kind: Receiver
+metadata:
+  name: receiver
+spec:
+  tolerations:
+  - key: key1
+    operator: Equal
+    value: value1
+    effect: NoSchedule
+
+  affinity:
+    nodeAffinity:
+      requiredDuringSchedulingIgnoredDuringExecution:
+        nodeSelectorTerms:
+        - matchExpressions:
+          - key: disktype
+            operator: In
+            values:
+            - ssd
+
+  topologySpreadConstraints:
+  - maxSkew: 1
+    topologyKey: zone
+    whenUnsatisfiable: DoNotSchedule
+    labelSelector:
+      matchLabels:
+        foo: bar
+
+  priorityClassName: ""
+
+  replicas: 1
+  maxSurge: 1
+  maxUnavailable: 1
+
+  resources:
+    requests:
+      cpu: 1
+      memory: 5Gi
+    limits:
+      cpu: 1
+      memory: 6Gi
+
+  readinessProbe:
+    initialDelaySeconds: 0
+    periodSeconds: 10
+    timeoutSeconds: 1
+    successThreshold: 1
+    failureThreshold: 3
+
+  livenessProbe:
+    initialDelaySeconds: 0
+    periodSeconds: 10
+    timeoutSeconds: 1
+    successThreshold: 1
+    failureThreshold: 3
+
+  service:
+    type: NodePort
+    httpPort: 80
+
+  credentialSecretRef: apache-credentials
+
+  containerImages:
+    imagePullSecrets: []
+    imagePullPolicy: Always
+    gerritImages:
+      registry: docker.io
+      org: k8sgerrit
+      tag: latest
+    busyBox:
+      registry: docker.io
+      tag: latest
+
+  storage:
+    storageClasses:
+      readWriteOnce: default
+      readWriteMany: shared-storage
+      nfsWorkaround:
+        enabled: false
+        chownOnStartup: false
+        idmapdConfig: |-
+          [General]
+            Verbosity = 0
+            Domain = localdomain.com
+
+          [Mapping]
+            Nobody-User = nobody
+            Nobody-Group = nogroup
+
+    sharedStorage:
+      externalPVC:
+        enabled: false
+        claimName: ""
+      size: 1Gi
+      volumeName: ""
+      selector:
+        matchLabels:
+          volume-type: ssd
+          aws-availability-zone: us-east-1
+
+  ingress:
+    host: example.com
+    tlsEnabled: false
+```
+
+## GitGarbageCollection
+
+---
+
+**Group**: gerritoperator.google.com \
+**Version**: v1alpha1 \
+**Kind**: GitGarbageCollection
+
+---
+
+
+| Field | Type | Description |
+|---|---|---|
+| `apiVersion` | `String` | APIVersion of this resource |
+| `kind` | `String` | Kind of this resource |
+| `metadata` | [`ObjectMeta`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#objectmeta-v1-meta) | Metadata of the resource |
+| `spec` | [`GitGarbageCollectionSpec`](#gitgarbagecollectionspec) | Specification for GitGarbageCollection |
+| `status` | [`GitGarbageCollectionStatus`](#gitgarbagecollectionstatus) | Status for GitGarbageCollection |
+
+Example:
+
+```yaml
+apiVersion: "gerritoperator.google.com/v1alpha1"
+kind: GitGarbageCollection
+metadata:
+  name: gitgc
+spec:
+  cluster: gerrit
+  schedule: "*/5 * * * *"
+
+  projects: []
+
+  resources:
+    requests:
+      cpu: 100m
+      memory: 256Mi
+    limits:
+      cpu: 100m
+      memory: 256Mi
+
+  tolerations:
+  - key: key1
+    operator: Equal
+    value: value1
+    effect: NoSchedule
+
+  affinity:
+    nodeAffinity:
+      requiredDuringSchedulingIgnoredDuringExecution:
+        nodeSelectorTerms:
+        - matchExpressions:
+          - key: disktype
+            operator: In
+            values:
+            - ssd
+```
+
+## GerritNetwork
+
+---
+
+**Group**: gerritoperator.google.com \
+**Version**: v1alpha2 \
+**Kind**: GerritNetwork
+
+---
+
+
+| Field | Type | Description |
+|---|---|---|
+| `apiVersion` | `String` | APIVersion of this resource |
+| `kind` | `String` | Kind of this resource |
+| `metadata` | [`ObjectMeta`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#objectmeta-v1-meta) | Metadata of the resource |
+| `spec` | [`GerritNetworkSpec`](#gerritnetworkspec) | Specification for GerritNetwork |
+
+Example:
+
+```yaml
+apiVersion: "gerritoperator.google.com/v1alpha2"
+kind: GerritNetwork
+metadata:
+  name: gerrit-network
+spec:
+  ingress:
+    enabled: true
+    host: example.com
+    annotations: {}
+    tls:
+      enabled: false
+      secret: ""
+  receiver:
+    name: receiver
+    httpPort: 80
+  primaryGerrit: {}
+    # name: gerrit-primary
+    # httpPort: 80
+    # httpPort: 29418
+  gerritReplica:
+    name: gerrit
+    httpPort: 80
+    httpPort: 29418
+```
+
+## GerritClusterSpec
+
+| Field | Type | Description |
+|---|---|---|
+| `storage` | [`GerritStorageConfig`](#gerritstorageconfig) | Storage used by Gerrit instances |
+| `containerImages` | [`ContainerImageConfig`](#containerimageconfig) | Container images used inside GerritCluster |
+| `ingress` | [`GerritClusterIngressConfig`](#gerritclusteringressconfig) | Ingress traffic handling in GerritCluster |
+| `refdb` | [`GlobalRefDbConfig`](#globalrefdbconfig) | The Global RefDB used by Gerrit |
+| `serverId` | `String` | The serverId to be used for all Gerrit instances (default: `<namespace>/<name>`) |
+| `gerrits` | [`GerritTemplate`](#gerrittemplate)-Array | A list of Gerrit instances to be installed in the GerritCluster. Only a single primary Gerrit and a single Gerrit Replica is permitted. |
+| `receiver` | [`ReceiverTemplate`](#receivertemplate) | A Receiver instance to be installed in the GerritCluster. |
+
+## GerritClusterStatus
+
+| Field | Type | Description |
+|---|---|---|
+| `members` | `Map<String, List<String>>` | A map listing all Gerrit and Receiver instances managed by the GerritCluster by name |
+
+## StorageConfig
+
+| Field | Type | Description |
+|---|---|---|
+| `storageClasses` | [`StorageClassConfig`](#storageclassconfig) | StorageClasses used in the GerritCluster |
+| `sharedStorage` | [`SharedStorage`](#sharedstorage) | Volume used for resources shared between Gerrit instances except git repositories |
+
+## GerritStorageConfig
+
+Extends [StorageConfig](#StorageConfig).
+
+| Field | Type | Description |
+|---|---|---|
+| `pluginCache` | [`PluginCacheConfig`](#plugincacheconfig) | Configuration of cache for downloaded plugins |
+
+## StorageClassConfig
+
+| Field | Type | Description |
+|---|---|---|
+| `readWriteOnce` | `String` | Name of a StorageClass allowing ReadWriteOnce access. (default: `default`) |
+| `readWriteMany` | `String` | Name of a StorageClass allowing ReadWriteMany access. (default: `shared-storage`) |
+| `nfsWorkaround` | [`NfsWorkaroundConfig`](#nfsworkaroundconfig) | NFS is not well supported by Kubernetes. These options provide a workaround to ensure correct file ownership and id mapping |
+
+## NfsWorkaroundConfig
+
+| Field | Type | Description |
+|---|---|---|
+| `enabled` | `boolean` | If enabled, below options might be used. (default: `false`) |
+| `chownOnStartup` | `boolean` | If enabled, the ownership of the mounted NFS volumes will be set on pod startup. Note that this is not done recursively. It is expected that all data already present in the volume was created by the user used in accessing containers. (default: `false`) |
+| `idmapdConfig` | `String` | The idmapd.config file can be used to e.g. configure the ID domain. This might be necessary for some NFS servers to ensure correct mapping of user and group IDs. (optional) |
+
+## SharedStorage
+
+| Field | Type | Description |
+|---|---|---|
+| `externalPVC` | [`ExternalPVCConfig`](#externalpvcconfig) | Configuration regarding the use of an external / manually created PVC |
+| `size` | [`Quantity`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#quantity-resource-core) | Size of the volume (mandatory) |
+| `volumeName` | `String` | Name of a specific persistent volume to claim (optional) |
+| `selector` | [`LabelSelector`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#labelselector-v1-meta) | Selector to select a specific persistent volume (optional) |
+
+## PluginCacheConfig
+
+| Field | Type | Description |
+|---|---|---|
+| `enabled` | `boolean` | If enabled, downloaded plugins will be cached. (default: `false`) |
+
+## ExternalPVCConfig
+
+| Field | Type | Description |
+|---|---|---|
+| `enabled` | `boolean` | If enabled, a provided PVC will be used instead of creating one. (default: `false`) |
+| `claimName` | `String` | Name of the PVC to be used. |
+
+## ContainerImageConfig
+
+| Field | Type | Description |
+|---|---|---|
+| `imagePullPolicy` | `String` | Image pull policy (https://kubernetes.io/docs/concepts/containers/images/#image-pull-policy) to be used in all containers. (default: `Always`) |
+| `imagePullSecrets` | [`LocalObjectReference`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#localobjectreference-v1-core)-Array | List of names representing imagePullSecrets available in the cluster. These secrets will be added to all pods. (optional) |
+| `busyBox` | [`BusyBoxImage`](#busyboximage) | The busybox container is used for some init containers |
+| `gerritImages` | [`GerritRepositoryConfig`](#gerritrepositoryconfig) | The container images in this project are tagged with the output of git describe. All container images are published for each version, even when the image itself was not updated. This ensures that all containers work well together. Here, the data on how to get those images can be configured. |
+
+## BusyBoxImage
+
+| Field | Type | Description |
+|---|---|---|
+| `registry` | `String` | The registry from which to pull the "busybox" image. (default: `docker.io`) |
+| `tag` | `String` | The tag/version of the "busybox" image. (default: `latest`) |
+
+## GerritRepositoryConfig
+
+| Field | Type | Description |
+|---|---|---|
+| `registry` | `String` | The registry from which to pull the images. (default: `docker.io`) |
+| `org` | `String` | The organization in the registry containing the images. (default: `k8sgerrit`) |
+| `tag` | `String` | The tag/version of the images. (default: `latest`) |
+
+## GerritClusterIngressConfig
+
+| Field | Type | Description |
+|---|---|---|
+| `enabled` | `boolean` | Whether to configure an ingress provider to manage the ingress traffic in the GerritCluster (default: `false`) |
+| `host` | `string` | Hostname to be used by the ingress. For each Gerrit deployment a new subdomain using the name of the respective Gerrit CustomResource will be used. |
+| `annotations` | `Map<String, String>` | Annotations to be set for the ingress. This allows to configure the ingress further by e.g. setting the ingress class. This will be only used for type INGRESS and ignored otherwise. (optional) |
+| `tls` | [`GerritIngressTlsConfig`](#gerritingresstlsconfig) | Configuration of TLS to be used in the ingress |
+| `ambassador` | [`GerritIngressAmbassadorConfig`](#gerritingressambassadorconfig) | Ambassador configuration. Only relevant when the INGRESS environment variable is set to "ambassador" in the operator |
+
+## GerritIngressTlsConfig
+
+| Field | Type | Description |
+|---|---|---|
+| `enabled` | `boolean` | Whether to use TLS (default: `false`) |
+| `secret` | `String` | Name of the secret containing the TLS key pair. The certificate should be a wildcard certificate allowing for all subdomains under the given host. |
+
+## GerritIngressAmbassadorConfig
+
+| Field | Type | Description |
+|---|---|---|
+| `id` | `List<String>` | The operator uses the ids specified in `ambassadorId` to set the [ambassador_id](https://www.getambassador.io/docs/edge-stack/1.14/topics/running/running#ambassador_id) spec field in the Ambassador CustomResources it creates (`Mapping`, `TLSContext`). (optional) |
+| `createHost`| `boolean` | Specify whether you want the operator to create a `Host` resource. This will be required if you don't have a wildcard host set up in your cluster. Default is `false`. (optional) |
+
+## GlobalRefDbConfig
+
+Note, that the operator will not deploy or operate the database used for the
+global refdb. It will only configure Gerrit to use it.
+
+| Field | Type | Description |
+|---|---|---|
+| `database` | [`RefDatabase`](#refdatabase) | Which database to use for the global refdb. Choices: `NONE`, `SPANNER`, `ZOOKEEPER`. (default: `NONE`) |
+| `spanner` | [`SpannerRefDbConfig`](#spannerrefdbconfig) | Configuration of spanner. Only used if spanner was configured to be used for the global refdb. |
+| `zookeeper` | [`ZookeeperRefDbConfig`](#zookeeperrefdbconfig) | Configuration of zookeeper. Only used, if zookeeper was configured to be used for the global refdb. |
+
+## RefDatabase
+
+| Value | Description|
+|---|---|
+| `NONE` | No global refdb will be used. Not allowed, if a primary Gerrit with 2 or more instances will be installed. |
+| `SPANNER` | Spanner will be used as a global refdb |
+| `ZOOKEEPER` | Zookeeper will be used as a global refdb |
+
+## SpannerRefDbConfig
+
+Note that the spanner ref-db plugin requires google credentials to be mounted to /var/gerrit/etc/gcp-credentials.json. Instructions for generating those credentials can be found [here](https://developers.google.com/workspace/guides/create-credentials) and may be provided in the optional secretRef in [`GerritTemplateSpec`](#gerrittemplatespec).
+
+| Field | Type | Description |
+|---|---|---|
+| `projectName` | `String` | Spanner project name to be used |
+| `instance` | `String` | Spanner instance name to be used |
+| `database` | `String` | Spanner database name to be used |
+
+## ZookeeperRefDbConfig
+
+| Field | Type | Description |
+|---|---|---|
+| `connectString` | `String` | Hostname and port of the zookeeper instance to be used, e.g. `zookeeper.example.com:2181` |
+| `rootNode` | `String` | Root node that will be used to store the global refdb data. Will be set automatically, if `GerritCluster` is being used. |
+
+## GerritTemplate
+
+| Field | Type | Description |
+|---|---|---|
+| `metadata` | [`ObjectMeta`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#objectmeta-v1-meta) | Metadata of the resource. A name is mandatory. Labels can optionally be defined. Other fields like the namespace are ignored. |
+| `spec` | [`GerritTemplateSpec`](#gerrittemplatespec) | Specification for GerritTemplate |
+
+## GerritTemplateSpec
+
+| Field | Type | Description |
+|---|---|---|
+| `serviceAccount` | `String` | ServiceAccount to be used by Gerrit. Required for service discovery when using the high-availability plugin |
+| `tolerations` | [`Toleration`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#toleration-v1-core)-Array | Pod tolerations (optional) |
+| `affinity` | [`Affinity`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#affinity-v1-core) | Pod affinity (optional) |
+| `topologySpreadConstraints` | [`TopologySpreadConstraint`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#topologyspreadconstraint-v1-core)-Array | Pod topology spread constraints (optional) |
+| `priorityClassName` | `String` | [PriorityClass](https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/) to be used with the pod (optional) |
+| `replicas` | `int` | Number of pods running Gerrit in the StatefulSet (default: 1) |
+| `updatePartition` | `int` | Ordinal at which to start updating pods. Pods with a lower ordinal will not be updated. (default: 0) |
+| `resources` | [`ResourceRequirements`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#resourcerequirements-v1-core) | Resource requirements for the Gerrit container |
+| `startupProbe` | [`GerritProbe`](#gerritprobe) | [Startup probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#configure-probes). The action will be set by the operator. All other probe parameters can be set. |
+| `readinessProbe` | [`GerritProbe`](#gerritprobe) | [Readiness probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#configure-probes). The action will be set by the operator. All other probe parameters can be set. |
+| `livenessProbe` | [`GerritProbe`](#gerritprobe) | [Liveness probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#configure-probes). The action will be set by the operator. All other probe parameters can be set. |
+| `gracefulStopTimeout` | `long` | Seconds the pod is allowed to shutdown until it is forcefully killed (default: 30) |
+| `service` | [`GerritServiceConfig`](#gerritserviceconfig) | Configuration for the service used to manage network access to the StatefulSet |
+| `site` | [`GerritSite`](#gerritsite) | Configuration concerning the Gerrit site directory |
+| `plugins` | [`GerritPlugin`](#gerritplugin)-Array | List of Gerrit plugins to install. These plugins can either be packaged in the Gerrit war-file or they will be downloaded. (optional) |
+| `libs` | [`GerritModule`](#gerritmodule)-Array | List of Gerrit library modules to install. These lib modules will be downloaded. (optional) |
+| `configFiles` | `Map<String, String>` | Configuration files for Gerrit that will be mounted into the Gerrit site's etc-directory (gerrit.config is mandatory) |
+| `secretRef` | `String` | Name of secret containing configuration files, e.g. secure.config, that will be mounted into the Gerrit site's etc-directory (optional) |
+| `mode` | [`GerritMode`](#gerritmode) | In which mode Gerrit should be run. (default: PRIMARY) |
+| `debug` | [`GerritDebugConfig`](#gerritdebugconfig) | Enable the debug-mode for Gerrit |
+
+## GerritProbe
+
+**Extends:** [`Probe`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#probe-v1-core)
+
+The fields `exec`, `grpc`, `httpGet` and `tcpSocket` cannot be set manually anymore
+compared to the parent object. All other options can still be configured.
+
+## GerritServiceConfig
+
+| Field | Type | Description |
+|---|---|---|
+| `type` | `String` | Service type (default: `NodePort`) |
+| `httpPort` | `int` | Port used for HTTP requests (default: `80`) |
+| `sshPort` | `Integer` | Port used for SSH requests (optional; if unset, SSH access is disabled). If Istio is used, the Gateway will be automatically configured to accept SSH requests. If an Ingress controller is used, SSH requests will only be served by the Service itself! |
+
+## GerritSite
+
+| Field | Type | Description |
+|---|---|---|
+| `size` | [`Quantity`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#quantity-resource-core) | Size of the volume used to persist not otherwise persisted site components (e.g. git repositories are persisted in a dedicated volume) (mandatory) |
+
+## GerritModule
+
+| Field | Type | Description |
+|---|---|---|
+| `name` | `String` | Name of the module/plugin |
+| `url` | `String` | URL of the module/plugin, if it should be downloaded. If the URL is not set, the plugin is expected to be packaged in the war-file (not possible for lib-modules). (optional) |
+| `sha1` | `String` | SHA1-checksum of the module/plugin JAR-file. (mandatory, if `url` is set) |
+
+## GerritPlugin
+
+**Extends:** [`GerritModule`](#gerritmodule)
+
+| Field | Type | Description |
+|---|---|---|
+| `installAsLibrary` | `boolean` | Some plugins also need to be installed as a library. If set to `true` the plugin JAR will be symlinked to the `lib`-directory in the Gerrit site. (default: `false`) |
+
+## GerritMode
+
+| Value | Description|
+|---|---|
+| `PRIMARY` | A primary Gerrit |
+| `REPLICA` | A Gerrit Replica, which only serves git fetch/clone requests |
+
+## GerritDebugConfig
+
+These options allow to debug Gerrit. It will enable debugging in all pods and
+expose the port 8000 in the container. Port-forwarding is required to connect the
+debugger.
+Note, that all pods will be restarted to enable the debugger. Also, if `suspend`
+is enabled, ensure that the lifecycle probes are configured accordingly to prevent
+pod restarts before Gerrit is ready.
+
+| Field | Type | Description |
+|---|---|---|
+| `enabled` | `boolean` | Whether to enable debugging. (default: `false`) |
+| `suspend` | `boolean` | Whether to suspend Gerrit on startup. (default: `false`) |
+
+## GerritSpec
+
+**Extends:** [`GerritTemplateSpec`](#gerrittemplatespec)
+
+| Field | Type | Description |
+|---|---|---|
+| `storage` | [`GerritStorageConfig`](#gerritstorageconfig) | Storage used by Gerrit instances |
+| `containerImages` | [`ContainerImageConfig`](#containerimageconfig) | Container images used inside GerritCluster |
+| `ingress` | [`IngressConfig`](#ingressconfig) | Ingress configuration for Gerrit |
+| `refdb` | [`GlobalRefDbConfig`](#globalrefdbconfig) | The Global RefDB used by Gerrit |
+| `serverId` | `String` | The serverId to be used for all Gerrit instances |
+
+## GerritStatus
+
+| Field | Type | Description |
+|---|---|---|
+| `ready` | `boolean` | Whether the Gerrit instance is ready |
+| `appliedConfigMapVersions` | `Map<String, String>` | Versions of each ConfigMap currently mounted into Gerrit pods |
+| `appliedSecretVersions` | `Map<String, String>` | Versions of each secret currently mounted into Gerrit pods |
+
+## IngressConfig
+
+| Field | Type | Description |
+|---|---|---|
+| `host` | `string` | Hostname that is being used by the ingress provider for this Gerrit instance. |
+| `tlsEnabled` | `boolean` | Whether the ingress provider enables TLS. (default: `false`) |
+
+## ReceiverTemplate
+
+| Field | Type | Description |
+|---|---|---|
+| `metadata` | [`ObjectMeta`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#objectmeta-v1-meta) | Metadata of the resource. A name is mandatory. Labels can optionally be defined. Other fields like the namespace are ignored. |
+| `spec` | [`ReceiverTemplateSpec`](#receivertemplatespec) | Specification for ReceiverTemplate |
+
+## ReceiverTemplateSpec
+
+| Field | Type | Description |
+|---|---|---|
+| `tolerations` | [`Toleration`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#toleration-v1-core)-Array | Pod tolerations (optional) |
+| `affinity` | [`Affinity`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#affinity-v1-core) | Pod affinity (optional) |
+| `topologySpreadConstraints` | [`TopologySpreadConstraint`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#topologyspreadconstraint-v1-core)-Array | Pod topology spread constraints (optional) |
+| `priorityClassName` | `String` | [PriorityClass](https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/) to be used with the pod (optional) |
+| `replicas` | `int` | Number of pods running the receiver in the Deployment (default: 1) |
+| `maxSurge` | `IntOrString` | Ordinal or percentage of pods that are allowed to be created in addition during rolling updates. (default: `1`) |
+| `maxUnavailable` | `IntOrString` | Ordinal or percentage of pods that are allowed to be unavailable during rolling updates. (default: `1`) |
+| `resources` | [`ResourceRequirements`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#resourcerequirements-v1-core) | Resource requirements for the Receiver container |
+| `readinessProbe` | [`ReceiverProbe`](#receiverprobe) | [Readiness probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#configure-probes). The action will be set by the operator. All other probe parameters can be set. |
+| `livenessProbe` | [`ReceiverProbe`](#receiverprobe) | [Liveness probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#configure-probes). The action will be set by the operator. All other probe parameters can be set. |
+| `service` | [`ReceiverServiceConfig`](#receiverserviceconfig) |  Configuration for the service used to manage network access to the Deployment |
+| `credentialSecretRef` | `String` | Name of the secret containing the .htpasswd file used to configure basic authentication within the Apache server (mandatory) |
+
+## ReceiverSpec
+
+**Extends:** [`ReceiverTemplateSpec`](#receivertemplatespec)
+
+| Field | Type | Description |
+|---|---|---|
+| `storage` | [`StorageConfig`](#storageconfig) | Storage used by Gerrit/Receiver instances |
+| `containerImages` | [`ContainerImageConfig`](#containerimageconfig) | Container images used inside GerritCluster |
+| `ingress` | [`IngressConfig`](#ingressconfig) | Ingress configuration for Gerrit |
+
+## ReceiverStatus
+
+| Field | Type | Description |
+|---|---|---|
+| `ready` | `boolean` | Whether the Receiver instance is ready |
+| `appliedCredentialSecretVersion` | `String` | Version of credential secret currently mounted into Receiver pods |
+
+## ReceiverProbe
+
+**Extends:** [`Probe`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#probe-v1-core)
+
+The fields `exec`, `grpc`, `httpGet` and `tcpSocket` cannot be set manually anymore
+compared to the parent object. All other options can still be configured.
+
+## ReceiverServiceConfig
+
+| Field | Type | Description |
+|---|---|---|
+| `type` | `String` | Service type (default: `NodePort`) |
+| `httpPort` | `int` | Port used for HTTP requests (default: `80`) |
+
+## GitGarbageCollectionSpec
+
+| Field | Type | Description |
+|---|---|---|
+| `cluster` | `string` | Name of the Gerrit cluster this Gerrit is a part of. (mandatory) |
+| `tolerations` | [`Toleration`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#toleration-v1-core)-Array | Pod tolerations (optional) |
+| `affinity` | [`Affinity`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#affinity-v1-core) | Pod affinity (optional) |
+| `schedule` | `string` | Cron schedule defining when to run git gc (mandatory) |
+| `projects` | `Set<String>` | List of projects to gc. If omitted, all projects not handled by other Git GC jobs will be gc'ed. Only one job gc'ing all projects can exist. (default: `[]`) |
+| `resources` | [`ResourceRequirements`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#resourcerequirements-v1-core) | Resource requirements for the GitGarbageCollection container |
+
+## GitGarbageCollectionStatus
+
+| Field | Type | Description |
+|---|---|---|
+| `replicateAll` | `boolean` | Whether this GitGarbageCollection handles all projects |
+| `excludedProjects` | `Set<String>` | List of projects that were excluded from this GitGarbageCollection, since they are handled by other Jobs |
+| `state` | [`GitGcState`](#gitgcstate) | State of the GitGarbageCollection |
+
+## GitGcState
+
+| Value | Description|
+|---|---|
+| `ACTIVE` | GitGarbageCollection is scheduled |
+| `INACTIVE` | GitGarbageCollection is not scheduled |
+| `CONFLICT` | GitGarbageCollection conflicts with another GitGarbageCollection |
+| `ERROR` | Controller failed to schedule GitGarbageCollection |
+
+## GerritNetworkSpec
+
+| Field | Type | Description |
+|---|---|---|
+| `ingress` | [`GerritClusterIngressConfig`](#gerritclusteringressconfig) | Ingress traffic handling in GerritCluster |
+| `receiver` | [`NetworkMember`](#networkmember) | Receiver in the network. |
+| `primaryGerrit` | [`NetworkMemberWithSsh`](#networkmemberwithssh) | Primary Gerrit in the network. |
+| `gerritReplica` | [`NetworkMemberWithSsh`](#networkmemberwithssh) | Gerrit Replica in the network. |
+
+## NetworkMember
+
+| Field      | Type     | Description                |
+|------------|----------|----------------------------|
+| `name`     | `String` | Name of the network member |
+| `httpPort` | `int`    | Port used for HTTP(S)      |
+
+## NetworkMemberWithSsh
+
+**Extends:** [`NetworkMember`](#networkmember)
+
+| Field     | Type  | Description       |
+|-----------|-------|-------------------|
+| `sshPort` | `int` | Port used for SSH |
diff --git a/charts/k8s-gerrit/Documentation/operator.md b/charts/k8s-gerrit/Documentation/operator.md
new file mode 100644
index 0000000..919e217
--- /dev/null
+++ b/charts/k8s-gerrit/Documentation/operator.md
@@ -0,0 +1,440 @@
+# Gerrit Operator
+
+1. [Gerrit Operator](#gerrit-operator)
+   1. [Build](#build)
+   2. [Versioning](#versioning)
+   3. [Publish](#publish)
+   4. [Tests](#tests)
+   5. [Prerequisites](#prerequisites)
+      1. [Shared Storage (ReadWriteMany)](#shared-storage-readwritemany)
+      2. [Ingress provider](#ingress-provider)
+   6. [Deploy](#deploy)
+      1. [Using helm charts](#using-helm-charts)
+         1. [gerrit-operator-crds](#gerrit-operator-crds)
+         2. [gerrit-operator](#gerrit-operator-1)
+      2. [Without the helm charts](#without-the-helm-charts)
+   7. [CustomResources](#customresources)
+      1. [GerritCluster](#gerritcluster)
+      2. [Gerrit](#gerrit)
+      3. [GitGarbageCollection](#gitgarbagecollection)
+      4. [Receiver](#receiver)
+      5. [GerritNetwork](#gerritnetwork)
+   8. [Configuration of Gerrit](#configuration-of-gerrit)
+
+## Build
+
+For this step, you need Java 11 and Maven installed.
+
+To build all components of the operator run:
+
+```sh
+cd operator
+mvn clean install
+```
+
+This step compiles the Java source code into `.class` bytecode files in a newly
+generated `operator/target` folder. A `gerrit-operator` image is also created
+locally. Moreover, the CRD helm chart is updated with the latest CRDs as part of
+this build step.
+
+The jar-version and container image tag can be set using the `revision` property:
+
+```sh
+mvn clean install -Drevision=$(git describe --always --dirty)
+```
+
+## Versioning
+
+The Gerrit Operator is still in an early state of development. The operator is
+thus at the moment not semantically versioned. The CustomResources are as of now
+independently versioned, i.e. the `GerritCluster` resource can have a different
+version than the `GitGarbageCollection` resource, although they are in the same
+group. At the moment, only the current version will be supported by the operator,
+i.e. there won't be a migration path. As soon as the API reaches some stability,
+this will change.
+
+## Publish
+
+Currently, there does not exist a container image for the operator in the
+`docker.io/k8sgerrit` registry. You must build your own image in order to run
+the operator in your cluster. To publish the container image of the Gerrit
+Operator:
+
+1. Update the `docker.registry` and `docker.org` tags in the `operator/pom.xml`
+file to point to your own Docker registry and org that you have permissions to
+push to.
+
+```xml
+<docker.registry>my-registry</docker.registry>
+<docker.org>my-org</docker.org>
+```
+
+2. run the following commands:
+
+```sh
+cd operator
+mvn clean install -P publish
+```
+
+This will build the operator source code, create an image out of the
+built artifacts, and publish this image to the registry specified in the
+`pom.xml` file. The built image is multi-platform - it will run on both `amd64`
+and `arm64` architectures. It is okay to run this build command from an ARM
+Mac.
+
+## Tests
+
+Executing the E2E tests has a few infrastructure requirements that have to be
+provided:
+
+- An (unused) Kubernetes cluster
+- The 'default' StorageClass that supports ReadWriteOnce access. It has to be
+  possible to provision volumes using this StorageClass.
+- A StorageClass that supports ReadWriteMany access. It has to be possible to
+  provision volumes using this StorageClass. Such a StorageClass could be provided
+  by the [NFS-subdir-provisioner chart](https://github.com/kubernetes-sigs/nfs-subdir-external-provisioner).
+- An [Nginx Ingress Controller](https://github.com/kubernetes/ingress-nginx)
+- An installation of [OpenLDAP](../supplements/test-cluster/ldap/openldap.yaml)
+  with at least one user.
+- Istio installed with the [profile](../istio/gerrit.profile.yaml) provided by
+  this project
+- A secret containing valid certificates for the given hostnames. For istio this
+  secret has to be named `tls-secret` and be present in the `istio-system` namespace.
+  For the Ingress controller, the secret has to be either set as the default
+  secret to be used or somehow automatically be provided in the namespaces created
+  by the tests and named `tls-secret`, e.g. by using Gardener to manage DNS and
+  certificates.
+
+A sample setup for components required in the cluster is provided under
+`$REPO_ROOT/supplements/test-cluster`. Some configuration has to be done manually
+(marked by `#TODO`) and the `deploy.sh`-script can be used to install/update all
+components.
+
+In addition, some properties have to be set to configure the tests:
+
+- `rwmStorageClass`: Name of the StorageClass providing RWM-access (default:nfs-client)
+- `registry`: Registry to pull container images from
+- `RegistryOrg`: Organization of the container images
+- `tag`: Container tag
+- `registryUser`: User for the container registry
+- `registryPwd`: Password for the container registry
+- `ingressDomain`: Domain to be used for the ingress
+- `istioDomain`: Domain to be used for istio
+- `ldapAdminPwd`: Admin password for LDAP server
+- `gerritUser`: Username of a user in LDAP
+- `gerritPwd`: The password of `gerritUser`
+
+The properties should be set in the `test.properties` file. Alternatively, a
+path of a properties file can be configured by using the
+`-Dproperties=<path to properties file>`-option.
+
+To run all E2E tests, use:
+
+```sh
+cd operator
+mvn clean install -P integration-test -Dproperties=<path to properties file>
+```
+
+Note, that running the E2E tests will also involve pushing the container image
+to the repository configured in the properties file.
+
+## Prerequisites
+
+Deploying Gerrit using the operator requires some additional prerequisites to be
+fulfilled:
+
+### Shared Storage (ReadWriteMany)
+
+Gerrit instances share the repositories and other data using shared volumes. Thus,
+a StorageClass and a suitable provisioner have to be available in the cluster.
+An example for such a provisioner would be the
+[NFS-subdir-external-provisioner](https://github.com/kubernetes-sigs/nfs-subdir-external-provisioner).
+
+### Ingress provider
+
+The Gerrit Operator will also set up network routing rules and an ingress point
+for the Gerrit instances it manages. The network routing rules ensure that requests
+will be routed to the intended GerritCluster component, e.g. in case a primary
+Gerrit and a Gerrit Replica exist in the cluster, git fetch/clone requests will
+be sent to the Gerrit Replica and all other requests to the primary Gerrit.
+
+You may specify the ingress provider by setting the `INGRESS` environment
+variable in the operator Deployment manifest. That is, the choice of an ingress
+provider is an operator-level setting. However, you may specify some ingress
+configuration options (host, tls, etc) at the `GerritCluster` level, via
+[GerritClusterIngressConfig](operator-api-reference.md#gerritclusteringressconfig).
+
+The Gerrit Operator currently supports the following Ingress providers:
+
+- **NONE**
+
+  The operator will install no Ingress components. Services will still be available.
+  No prerequisites are required for this case.
+
+  If `spec.ingress.enabled` is set to `true` in GerritCluster, the operator will
+  still configure network related options like `http.listenUrl` in Gerrit based on
+  the other options in `spec.ingress`.
+
+- **INGRESS**
+
+  The operator will install an Ingress. Currently only the
+  [Nginx-Ingress-Controller](https://docs.nginx.com/nginx-ingress-controller/) is
+  supported, which will have to be installed in the cluster and has to be configured
+  to [allow snippet configurations](https://docs.nginx.com/nginx-ingress-controller/configuration/ingress-resources/advanced-configuration-with-snippets/).
+  An example of a working deployment can be found [here](../supplements/test-cluster/ingress/).
+
+  SSH support is not fully managed by the operator, since it has to be enabled and
+  [configured in the nginx ingress controller itself](https://kubernetes.github.io/ingress-nginx/user-guide/exposing-tcp-udp-services/).
+
+- **ISTIO**
+
+  The operator supports the use of [Istio](https://istio.io/) as a service mesh.
+  An example on how to set up Istio can be found [here](../istio/gerrit.profile.yaml).
+
+- **AMBASSADOR**
+
+  The operator also supports [Ambassador](https://www.getambassador.io/) for
+  setting up ingress to the Gerrits deployed by the operator. If you use
+  Ambassador's "Edge Stack" or "Emissary Ingress" to provide ingress to your k8s
+  services, you should set INGRESS=AMBASSADOR. Currently, SSH is not directly
+  supported when using INGRESS=AMBASSADOR.
+
+
+## Deploy
+You will need to have admin privileges for your k8s cluster in order to be able
+to deploy the following resources.
+
+You may choose to deploy the operator resources using helm, or directly via
+`kubectl apply`.
+
+### Using helm charts
+Make sure you have [helm](https://helm.sh/) installed in your environment.
+
+There are two relevant helm charts.
+
+#### gerrit-operator-crds
+
+This chart installs the CRDs (k8s API extensions) to your k8s cluster. No chart
+values need to be modified. The build initiated by the `mvn install` command
+from the [Publish](#publish) section includes a step that updates the CRDs in
+this helm chart to reflect any changes made to them in the operator source code.
+The CRDs installed are: GerritCluster, Gerrit, GitGarbageCollection, Receiver.
+
+You do not need to manually `helm install` this chart; this chart is installed
+as a dependency of the second `gerrit-operator` helm chart as described in the
+next subheading.
+
+#### gerrit-operator
+
+This chart installs the `gerrit-operator-crds` chart as a dependency, and the
+following k8s resources:
+- Deployment
+- ServiceAccount
+- ClusterRole
+- ClusterRoleBinding
+
+The operator itself creates a Service resource and a
+ValidationWebhookConfigurations resource behind the scenes.
+
+You will need to modify the values in `helm-charts/gerrit-operator/values.yaml`
+to point the chart to the registry/org that is hosting the Docker container
+image for the operator (from the [Publish](#publish) step earlier). Now,
+
+run:
+```sh
+# Create a namespace for the gerrit-operator
+kubectl create ns gerrit-operator
+
+# Build the gerrit-operator-crds chart and store it in the charts/ subdirectory
+helm dependency build helm-charts/gerrit-operator/
+
+# Install the gerrit-operator-crds chart and the gerrit-operator chart
+helm -n gerrit-operator install gerrit-operator helm-charts/gerrit-operator/
+```
+
+The chart itself, and all the bundled namespaced resources, are installed in the
+`gerrit-operator` namespace, as per the `-n` option in the helm command.
+
+### Without the helm charts
+
+First all CustomResourceDefinitions have to be deployed:
+
+```sh
+kubectl apply -f operator/target/classes/META-INF/fabric8/*-v1.yml
+```
+
+Note that these do not include the -v1beta1.yaml files, as those are for old
+Kubernetes versions.
+
+The operator requires a Java Keystore with a keypair inside to allow TLS
+verification for Kubernetes Admission Webhooks. To create a keystore and
+encode it with base64, run:
+
+```sh
+keytool \
+  -genkeypair \
+  -alias operator \
+  -keystore keystore \
+  -keyalg RSA \
+  -keysize 2048 \
+  -validity 3650
+cat keystore | base64 -b 0
+```
+
+Add the result to the Secret in `k8s/operator.yaml` (see comments in the file)
+and also add the base64-encoded password for the keystore to the secret.
+
+Then the operator and associated RBAC rules can be deployed:
+
+```sh
+kubectl apply -f operator/k8s/rbac.yaml
+kubectl apply -f operator/k8s/operator.yaml
+```
+
+`k8s/operator.yaml` contains a basic deployment of the operator. Resources,
+docker image name etc. might have to be adapted. For example, the ingress
+provider has to be configured by setting the `INGRESS` environment variable
+in `operator/k8s/operator.yaml` to either `NONE`, `INGRESS`, `ISTIO`, or
+`AMBASSADOR`.
+
+## CustomResources
+
+The operator manages several CustomResources that are described in more detail
+below.
+
+The API reference for all CustomResources can be found [here](operator-api-reference.md).
+
+### GerritCluster
+
+The GerritCluster CustomResource installs one or multiple Gerrit instances. The
+operator takes over managing the state of all Gerrit instances within the cluster
+and ensures that the state stays in sync. To this end it manages additional
+resources that are shared between Gerrit instances or are required to synchronize
+the state between Gerrit instances. These additional resources include:
+
+- storage
+- network / service mesh
+
+Installing Gerrit with the GerritCluster resource is highly recommended over using
+the [Gerrit](#gerrit) CustomResource directly, even if only a single deployment is
+installed, since this reduces the requirements that have to be managed manually.
+The same holds true for the [Receiver](#receiver) CustomResource, which without
+a Gerrit instance using the same site provides little value.
+
+For now, only a single Gerrit CustomResource using each [mode](./operator-api-reference.md#gerritmode)
+can be deployed in a GerritCluster, e.g. one primary Gerrit and one Gerrit Replica.
+The reason for that is, that there is currently no sharding implemented and thus
+multiple deployments don't bring any more value than just scaling the existing
+deployment. Instead of a primary Gerrit also a Receiver can be installed.
+
+### Gerrit
+
+The Gerrit CustomResource deploys a Gerrit, which can run in multiple modes.
+
+The Gerrit-CustomResource is mainly meant to be used by the GerritCluster-reconciler
+to install Gerrit-instances managed by a GerritCluster. Gerrit-CustomResources
+can however also be applied separately. Note, that the Gerrit operator will then
+not create any storage resources or setup any network resources in addition to
+the service.
+
+### GitGarbageCollection
+
+The GitGarbageCollection-CustomResource is used by the operator to set up CronJobs
+that regularly run Git garbage collection on the git repositories that are served
+by a GerritCluster.
+
+A GitGarbageCollection can either handle all repositories, if no specific repository
+is configured or a selected set of repositories. Multiple GitGarbageCollections
+can exist as part of the same GerritCluster, but no two GitGarbageCollections
+can work on the same project. This is prevented in three ways:
+
+- ValidationWebhooks will prohibit the creation of a second GitGarbageCollection
+  that does not specify projects, i.e. that would work on all projects.
+- Projects for which a GitGarbageCollections that specifically selects it exists
+  will be excluded from the GitGarbageCollection that works on all projects, if
+  it exists.
+- ValidationWebhooks will prohibit the creation of a GitGarbageCollection that
+  specifies a project that was already specified by another GitGarbageCollection.
+
+### Receiver
+
+**NOTE:** A Receiver should never be installed for a GerritCluster that is already
+managing a primary Gerrit to avoid conflicts when writing into repositories.
+
+The Receiver-CustomResource installs a Deployment running Apache with a git-http-
+backend that is meant to receive pushes performed by Gerrit's replication plugin.
+It can only be installed into a GerritCluster that does not include a primary
+Gerrit, but only Gerrit Replicas.
+
+The Receiver-CustomResource is mainly meant to be used by the GerritCluster-reconciler
+to install a Receiver-instance managed by a GerritCluster. Receiver-CustomResources
+can however also be applied separately. Note, that the Gerrit operator will then
+not create any storage resources or setup any network resources in addition to
+the service.
+
+### GerritNetwork
+
+The GerritNetwork CustomResource deploys network components depending on the
+configured ingress provider to enable ingress traffic to GerritCluster components.
+
+The GerritNetwork CustomResource is not meant to be installed manually, but will
+be created by the Gerrit Operator based on the GerritCluster CustomResource.
+
+## Configuration of Gerrit
+
+The operator takes care of all configuration in Gerrit that depends on the
+infrastructure, i.e. Kubernetes and the GerritCluster. This avoids duplicated
+configuration and misconfiguration.
+
+This means that some options in the gerrit.config are not allowed to be changed.
+If these values are set and are not matching the expected value, a ValidationWebhook
+will reject the resource creation/update. Thus, it is best to not set these values
+at all. To see which values the operator assigned check the ConfigMap created by
+the operator for the respective Gerrit.
+
+These options are:
+
+- `cache.directory`
+
+    This should stay in the volume mounted to contain the Gerrit site and will
+    thus be set to `cache`.
+
+- `container.javaHome`
+
+    This has to be set to `/usr/lib/jvm/java-11-openjdk-amd64`, since this is
+    the path of the Java installation in the container.
+
+- `container.javaOptions = -Djavax.net.ssl.trustStore`
+
+    The keystore will be mounted to `/var/gerrit/etc/keystore`.
+
+- `container.replica`
+
+    This has to be set in the Gerrit-CustomResource under `spec.isReplica`.
+
+- `container.user`
+
+    The technical user in the Gerrit container is called `gerrit`.
+
+- `gerrit.basePath`
+
+    The git repositories are mounted to `/var/gerrit/git` in the container.
+
+- `gerrit.canonicalWebUrl`
+
+    The canonical web URL has to be set to the hostname used by the Ingress/Istio.
+
+- `httpd.listenURL`
+
+    This has to be set to `proxy-http://*:8080/` or `proxy-https://*:8080`,
+    depending of TLS is enabled in the Ingress or not, otherwise the Jetty
+    servlet will run into an endless redirect loop.
+
+- `sshd.advertisedAddress`
+
+    This is only enforced, if Istio is enabled. It can be configured otherwise.
+
+- `sshd.listenAddress`
+
+    Since the container port for SSH is fixed, this will be set automatically.
+    If no SSH port is configured in the service, the SSHD is disabled.
diff --git a/charts/k8s-gerrit/Documentation/roadmap.md b/charts/k8s-gerrit/Documentation/roadmap.md
new file mode 100644
index 0000000..9af175a
--- /dev/null
+++ b/charts/k8s-gerrit/Documentation/roadmap.md
@@ -0,0 +1,207 @@
+# Roadmap
+
+## General
+
+### Planned features
+
+- **Automated verification process**: Run tests automatically to verify changes. \
+  \
+  Most tests in the project require a Kubernetes cluster and some additional
+  prerequisites, e.g. istio. Currently, the Gerrit OpenSOurce community does not
+  have these resources. At SAP, we plan to run verification in our internal systems,
+  which won't be publicly viewable, but could already vote. Builds would only
+  be triggered, if a maintainer votes `+1` on the `Build-Approved`-label. \
+  \
+  Builds can be moved to a public CI at a later point in time.
+
+- **Automated publishing of container images**: Publishing container images will
+  happen automatically on ref-updated using a CI.
+
+- **Support for multiple Gerrit versions**: All currently supported Gerrit versions
+  will also be supported in k8s-gerrit. \
+  \
+  Currently, container images used by this project are only published for a single
+  Gerrit version, which is updated on an irregular schedule. Introducing stable
+  branches for each gerrit version will allow to maintain container images for
+  multiple Gerrit versions. Gerrit binaries will be updated with each official
+  release and more frequently on `master`. This will be (at least partially)
+  automated.
+
+- **Integration test suite**: A test suite that can be used to test a GerritCluster. \
+  \
+  A GerritCluster running in a Kubernetes cluster consists of multiple components.
+  Having a suite of automated tests would greatly help to verify deployments in
+  development landscapes before going productive.
+
+## Gerrit Operator
+
+### Version 1.0
+
+#### Implemented features
+
+- **High-availability**: Primary Gerrit StatefulSets will have limited support for
+  horizontal scaling. \
+  \
+  Scaling has been enabled using the [high-availability plugin](https://gerrit.googlesource.com/plugins/high-availability/).
+  Primary Gerrits will run in Active/Active configuration. Currently, two primary
+  Gerrit instances, i.e. 2 pods in a StatefulSet, are supported
+
+- **Global RefDB support**: Global RefDB is required for Active/Active configurations
+  of multiple primary Gerrits. \
+  \
+  The [Global RefDB](https://gerrit.googlesource.com/modules/global-refdb) support
+  is required for high-availability as described in the previous point. The
+  Gerrit Operator automatically sets up Gerrit to use a Global RefDB
+  implementation. The following implementations are supported:
+  - [spanner-refdb](https://gerrit.googlesource.com/plugins/spanner-refdb)
+  - [zookeeper-refdb](https://gerrit.googlesource.com/plugins/zookeeper-refdb)
+
+  \
+  The Gerrit Operator does not set up the database used for the Global RefDB. It
+  does however manage plugin/module installation and configuration in Gerrit.
+
+- **Full support for Nginx**: The integration of Ingresses managed by the Nginx
+  ingress controller now supports automated routing. \
+  \
+  Instead of requiring users to use different subdomains for the different Gerrit
+  deployments in the GerritCluster, requests are now automatically routed to the
+  respective deployments. SSH has still to be set up manually, since this requires
+  setting up the routing in the Nginx ingress controller itself.
+
+#### Planned features
+
+- **Versioning of CRDs**: Provide migration paths between API changes in CRDs. \
+  \
+  At the moment updates to the CRD are done without providing a migration path.
+  This means a complete reinstallation of CRDS, Operator, CRs and dependent resources
+  is required. This is not acceptable in a productive environment. Thus,
+  the operator will always support the last two versions of each CRD, if applicable,
+  and provide a migration path between those versions.
+
+- **Log collection**: Support addition of sidecar running a log collection agent
+  to send logs of all components to some logging stack. \
+  \
+  Planned supported log collectors:
+  - [OpenTelemetry agent](https://opentelemetry.io/docs/collector/deployment/agent/)
+  - Option to add a custom sidecar
+
+- **Support for additional Ingress controllers**: Add support for setting up routing
+  configurations for additional Ingress controllers \
+  \
+  Additional ingress controllers might include:
+  - [Ambassador](https://www.getambassador.io/products/edge-stack/api-gateway)
+
+### Version 1.x
+
+#### Potential features
+
+- **Support for additional log collection agents**: \
+  \
+  Additional log collection agents might include:
+  - fluentbit
+  - Option to add a custom sidecar
+
+- **Additional ValidationWebhooks**: Proactively avoid unsupported configurations. \
+  \
+  ValidationWebhooks are already used to avoid accepting unsupported configurations,
+  e.g. deploying more than one primary Gerrit CustomResource per GerritCluster.
+  So far not all such cases are covered. Thus, the set of validations will be
+  further expanded.
+
+- **Better test coverage**: More tests are required to find bugs earlier.
+
+- **Automated reload of plugins**: Reload plugins on configuration change. \
+  \
+  Configuration changes in plugins typically don't require a restart of Gerrit,
+  but just to reload the plugin. To avoid unnecessary downtime of pods, the
+  Gerrit Operator will only reload affected plugins and not restart all pods, if
+  only the plugin's configuration changed.
+
+- **Externalized (re-)indexing**: Alleviate load caused by online reindexing. \
+  \
+  On large Gerrit sites online reindexing due to schema migrations `a)` or initialization `b)`
+  of a new site might take up to weeks and use a lot of resources, which might
+  cause performance issues. This is not acceptable in production. The current
+  plan to solve this issue is to implement a separate Gerrit deployment (GerritIndexer)
+  that is not exposed to clients and that takes over the task of online reindexing.
+  The GerritIndexer will mount the same repositories and will share events via
+  the high-availability plugin. However, it will access repositories in read-only
+  mode. \
+  This solves the above named scenarios as follows: \
+  \
+  a) **Schema migrations**: If a Gerrit update including a schema migration for
+    an index is applied, the Gerrit instances serving clients will be configured
+    to continue to use the old schema. Online reindexing will be disabled in
+    those instances. The GerritIndexer will have online reindexing enabled and
+    will start to build the new index version. As soon as it is finished, i.e.
+    it could start to use the new index version as read index, it will make a
+    copy of the new index and publish it, e.g. using a shared filesystem. A
+    restart of the Gerrit instances serving other clients will be triggered.
+    During this restart the new index will be copied into the site. Since there
+    may have been updated index entries since the new index version was published
+    indexing of entries updated in the meantime will be triggered. \
+  \
+  b) **Initialization of a new site**: If Gerrit is horizontally scaled, it will
+    be started with an empty index, i.e. it has to build the complete index. To
+    avoid this, the GerritIndexer deployment will continuously keep a copy of the
+    indexes up-to-date. It will regularly be stopped and a copy of the index will
+    be stored in a shared volume. This can be used as a base for new instances, which
+    then only have to update index entries that were changed in the meantime.
+
+- **Autoscaling**: Automatically scale Gerrit deployments based on usage. \
+  \
+  Metrics like available workers in the thread pools could be used to decide to
+  scale the Gerrit deployment horizontally. This would allow to dynamically adapt
+  to the current load. This helps to save costs and resources.
+
+### Version 2.0
+
+#### Potential features
+
+- **Multi region support**: Support setups that are distributed over multiple regions. \
+  \
+  Supporting Gerrit installations that are distributed over multiple regions would
+  allow to serve clients all over the world without large differences in latency
+  and would also improve availability and reduce the risks of data loss. \
+  Such a setup could be achieved by using the [multi-site setup](https://gerrit.googlesource.com/plugins/multi-site/).
+
+- **Remove the dependency on shared storage**: Use completely independent sites
+  instead of sharing a filesystem for some site components. \
+  \
+  NFS and other shared filesystems potentially might cause performance issues on
+  larger Gerrit installations due to latencies. A potential solution might be
+  to use the [multi-site setup](https://gerrit.googlesource.com/plugins/multi-site/)
+  to separate the sites of all instances and to use events and replication to
+  share the state
+
+- **Shared index**: Using an external centralized index, e.g. OpenSearch instead
+  of x copies of a Lucene index. \
+  \
+  Maintaining x copies of an index, where x is the number of Gerrit instances in
+  a gerritCluster, is unnecessarily expensive, since the same write transactions
+  have to be potentially done x times. Using a single centralized index would
+  resolve this issue.
+
+- **Shared cache**: Using an external centralized cache for all Gerrit instances. \
+  \
+  Using a single cache for all Gerrit instances will reduce the number of
+  computations for each Gerrit instance, since not every instance will have to
+  keep its own copy up-to-date.
+
+- **Sharding**: Shard a site based on repositories. \
+  \
+  Repositories served by a single GerritCluster might be quite diverse, e.g. ranging
+  from a few kilobytes to several gigabytes or repositories seeing high traffic
+  and other barely being fetched. It is not trivial to configure Gerrit to work
+  optimally for all repositories. Being able to shard at least the Gerrit Replicas
+  would help to optimally serve all repositories.
+
+## Helm charts
+
+Only limited support is planned for the `gerrit` and `gerrit-replica` helm-charts
+as soon as the Gerrit Operator reaches version 1.0. The reason is that the double
+maintenance of all features would not be feasible with the current number of
+contributors. The Gerrit Operator will support all features that are provided by
+the helm charts. If community members would like to adopt maintainership of the
+helm-charts, this would be very much appreciated and the helm-charts could then
+continued to be supported.
diff --git a/charts/k8s-gerrit/Jenkinsfile b/charts/k8s-gerrit/Jenkinsfile
new file mode 100644
index 0000000..c74570e
--- /dev/null
+++ b/charts/k8s-gerrit/Jenkinsfile
@@ -0,0 +1 @@
+k8sGerritPipeline()
diff --git a/charts/k8s-gerrit/LICENSE b/charts/k8s-gerrit/LICENSE
new file mode 100644
index 0000000..27bdfb6
--- /dev/null
+++ b/charts/k8s-gerrit/LICENSE
@@ -0,0 +1,317 @@
+
+```
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright 2018 The Android Open Source Project
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+```
+
+## Subcomponents
+
+This project includes the following subcomponents that are subject to separate
+license terms. Your use of these subcomponents is subject to the separate
+license terms applicable to each subcomponent.
+
+Docker-Py \
+https://github.com/docker/docker-py \
+Copyright 2016 Docker, Inc \
+Apache 2 license (https://github.com/docker/docker-py/blob/master/LICENSE)
+
+Kubernetes Python CLient \
+https://github.com/kubernetes-client/python \
+Copyright (c) 2014 The Kubernetes Authors \
+Apache 2 license (https://github.com/kubernetes-client/python/blob/master/LICENSE)
+
+Passlib \
+https://bitbucket.org/ecollins/passlib/wiki/Home \
+Copyright (c) 2008-2017 Assurance Technologies, LLC.
+All rights reserved. \
+3-Clause BSD License (https://passlib.readthedocs.io/en/stable/copyright.html)
+
+PyGit2 \
+https://github.com/libgit2/pygit2 \
+pygit2 is Copyright (C) the pygit2 contributors unless otherwise stated. \
+GPL2 (https://github.com/libgit2/pygit2/blob/master/COPYING)
+
+pyOpenSSL \
+https://github.com/pyca/pyopenssl \
+Copyright (c) 2001 The pyOpenSSL developers \
+Apache 2 license (https://github.com/pyca/pyopenssl/blob/master/LICENSE)
+
+PyTest \
+https://github.com/pytest-dev/pytest \
+Copyright (c) 2004-2017 Holger Krekel and others \
+MIT License (https://github.com/pytest-dev/pytest/blob/master/LICENSE)
+
+python-chromedriver-autoinstaller \
+https://github.com/yeongbin-jo/python-chromedriver-autoinstaller \
+Copyright (c) 2022 Yeongbin Jo \
+MIT License (https://github.com/yeongbin-jo/python-chromedriver-autoinstaller/blob/master/LICENSE)
+
+Requests \
+https://github.com/requests/requests \
+Copyright 2018 Kenneth Reitz \
+Apache 2 license (https://github.com/requests/requests/blob/master/LICENSE)
+
+Selenium \
+https://github.com/SeleniumHQ/selenium \
+Copyright 2022 Software Freedom Conservancy (SFC) \
+Apache 2 license (https://github.com/SeleniumHQ/selenium/blob/trunk/LICENSE)
+
+Ambassador \
+https://github.com/emissary-ingress/emissary \
+Copyright 2021 Ambassador Labs \
+Apache 2 license (https://github.com/emissary-ingress/emissary/blob/master/LICENSE)
+
+---
+## The MIT License (MIT)
+
+```
+Copyright <YEAR> <COPYRIGHT HOLDER>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+## 3-Clause BSD License
+
+```
+Copyright <YEAR> <COPYRIGHT HOLDER>
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation and/or
+other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors
+may be used to endorse or promote products derived from this software without
+specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+```
diff --git a/charts/k8s-gerrit/Pipfile b/charts/k8s-gerrit/Pipfile
new file mode 100644
index 0000000..18ace64
--- /dev/null
+++ b/charts/k8s-gerrit/Pipfile
@@ -0,0 +1,26 @@
+[[source]]
+name = "pypi"
+url = "https://pypi.org/simple"
+verify_ssl = true
+
+[dev-packages]
+pylint = "~=2.17.5"
+black = "~=23.7.0"
+
+[packages]
+docker = "~=6.1.3"
+pytest = "~=7.4.0"
+passlib = "~=1.7.4"
+pyopenssl = "~=23.2.0"
+requests = "~=2.31.0"
+pytest-timeout = "~=2.1.0"
+kubernetes = "~=27.2.0"
+pygit2 = "~=1.12.2"
+selenium = "~=4.11.2"
+chromedriver-autoinstaller = "==0.6.2"
+
+[requires]
+python_version = "3.11"
+
+[pipenv]
+allow_prereleases = true
diff --git a/charts/k8s-gerrit/Pipfile.lock b/charts/k8s-gerrit/Pipfile.lock
new file mode 100644
index 0000000..9678c9a
--- /dev/null
+++ b/charts/k8s-gerrit/Pipfile.lock
@@ -0,0 +1,834 @@
+{
+    "_meta": {
+        "hash": {
+            "sha256": "db93e37abb75873f53120e5f4871bead84d2c21f587da243a9d7729d4ed00a55"
+        },
+        "pipfile-spec": 6,
+        "requires": {
+            "python_version": "3.11"
+        },
+        "sources": [
+            {
+                "name": "pypi",
+                "url": "https://pypi.org/simple",
+                "verify_ssl": true
+            }
+        ]
+    },
+    "default": {
+        "attrs": {
+            "hashes": [
+                "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04",
+                "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==23.1.0"
+        },
+        "cachetools": {
+            "hashes": [
+                "sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590",
+                "sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==5.3.1"
+        },
+        "certifi": {
+            "hashes": [
+                "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082",
+                "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==2023.7.22"
+        },
+        "cffi": {
+            "hashes": [
+                "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5",
+                "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef",
+                "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104",
+                "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426",
+                "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405",
+                "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375",
+                "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a",
+                "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e",
+                "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc",
+                "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf",
+                "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185",
+                "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497",
+                "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3",
+                "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35",
+                "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c",
+                "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83",
+                "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21",
+                "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca",
+                "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984",
+                "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac",
+                "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd",
+                "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee",
+                "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a",
+                "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2",
+                "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192",
+                "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7",
+                "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585",
+                "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f",
+                "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e",
+                "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27",
+                "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b",
+                "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e",
+                "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e",
+                "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d",
+                "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c",
+                "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415",
+                "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82",
+                "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02",
+                "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314",
+                "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325",
+                "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c",
+                "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3",
+                "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914",
+                "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045",
+                "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d",
+                "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9",
+                "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5",
+                "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2",
+                "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c",
+                "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3",
+                "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2",
+                "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8",
+                "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d",
+                "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d",
+                "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9",
+                "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162",
+                "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76",
+                "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4",
+                "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e",
+                "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9",
+                "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6",
+                "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b",
+                "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01",
+                "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"
+            ],
+            "version": "==1.15.1"
+        },
+        "charset-normalizer": {
+            "hashes": [
+                "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96",
+                "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c",
+                "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710",
+                "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706",
+                "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020",
+                "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252",
+                "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad",
+                "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329",
+                "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a",
+                "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f",
+                "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6",
+                "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4",
+                "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a",
+                "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46",
+                "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2",
+                "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23",
+                "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace",
+                "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd",
+                "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982",
+                "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10",
+                "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2",
+                "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea",
+                "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09",
+                "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5",
+                "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149",
+                "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489",
+                "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9",
+                "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80",
+                "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592",
+                "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3",
+                "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6",
+                "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed",
+                "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c",
+                "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200",
+                "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a",
+                "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e",
+                "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d",
+                "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6",
+                "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623",
+                "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669",
+                "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3",
+                "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa",
+                "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9",
+                "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2",
+                "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f",
+                "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1",
+                "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4",
+                "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a",
+                "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8",
+                "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3",
+                "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029",
+                "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f",
+                "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959",
+                "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22",
+                "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7",
+                "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952",
+                "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346",
+                "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e",
+                "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d",
+                "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299",
+                "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd",
+                "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a",
+                "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3",
+                "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037",
+                "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94",
+                "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c",
+                "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858",
+                "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a",
+                "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449",
+                "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c",
+                "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918",
+                "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1",
+                "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c",
+                "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac",
+                "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"
+            ],
+            "markers": "python_full_version >= '3.7.0'",
+            "version": "==3.2.0"
+        },
+        "chromedriver-autoinstaller": {
+            "hashes": [
+                "sha256:7055e3e5a64e4352855fafab15d266e2ed325620222224fb261a2131e821dfe3",
+                "sha256:8ff5c715160b294c9e7cc0fae5ecc5ccaff5563ca1405daed6b959cca606e57c"
+            ],
+            "index": "pypi",
+            "version": "==0.6.2"
+        },
+        "cryptography": {
+            "hashes": [
+                "sha256:0d09fb5356f975974dbcb595ad2d178305e5050656affb7890a1583f5e02a306",
+                "sha256:23c2d778cf829f7d0ae180600b17e9fceea3c2ef8b31a99e3c694cbbf3a24b84",
+                "sha256:3fb248989b6363906827284cd20cca63bb1a757e0a2864d4c1682a985e3dca47",
+                "sha256:41d7aa7cdfded09b3d73a47f429c298e80796c8e825ddfadc84c8a7f12df212d",
+                "sha256:42cb413e01a5d36da9929baa9d70ca90d90b969269e5a12d39c1e0d475010116",
+                "sha256:4c2f0d35703d61002a2bbdcf15548ebb701cfdd83cdc12471d2bae80878a4207",
+                "sha256:4fd871184321100fb400d759ad0cddddf284c4b696568204d281c902fc7b0d81",
+                "sha256:5259cb659aa43005eb55a0e4ff2c825ca111a0da1814202c64d28a985d33b087",
+                "sha256:57a51b89f954f216a81c9d057bf1a24e2f36e764a1ca9a501a6964eb4a6800dd",
+                "sha256:652627a055cb52a84f8c448185922241dd5217443ca194d5739b44612c5e6507",
+                "sha256:67e120e9a577c64fe1f611e53b30b3e69744e5910ff3b6e97e935aeb96005858",
+                "sha256:6af1c6387c531cd364b72c28daa29232162010d952ceb7e5ca8e2827526aceae",
+                "sha256:6d192741113ef5e30d89dcb5b956ef4e1578f304708701b8b73d38e3e1461f34",
+                "sha256:7efe8041897fe7a50863e51b77789b657a133c75c3b094e51b5e4b5cec7bf906",
+                "sha256:84537453d57f55a50a5b6835622ee405816999a7113267739a1b4581f83535bd",
+                "sha256:8f09daa483aedea50d249ef98ed500569841d6498aa9c9f4b0531b9964658922",
+                "sha256:95dd7f261bb76948b52a5330ba5202b91a26fbac13ad0e9fc8a3ac04752058c7",
+                "sha256:a74fbcdb2a0d46fe00504f571a2a540532f4c188e6ccf26f1f178480117b33c4",
+                "sha256:a983e441a00a9d57a4d7c91b3116a37ae602907a7618b882c8013b5762e80574",
+                "sha256:ab8de0d091acbf778f74286f4989cf3d1528336af1b59f3e5d2ebca8b5fe49e1",
+                "sha256:aeb57c421b34af8f9fe830e1955bf493a86a7996cc1338fe41b30047d16e962c",
+                "sha256:ce785cf81a7bdade534297ef9e490ddff800d956625020ab2ec2780a556c313e",
+                "sha256:d0d651aa754ef58d75cec6edfbd21259d93810b73f6ec246436a21b7841908de"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==41.0.3"
+        },
+        "docker": {
+            "hashes": [
+                "sha256:aa6d17830045ba5ef0168d5eaa34d37beeb113948c413affe1d5991fc11f9a20",
+                "sha256:aecd2277b8bf8e506e484f6ab7aec39abe0038e29fa4a6d3ba86c3fe01844ed9"
+            ],
+            "index": "pypi",
+            "version": "==6.1.3"
+        },
+        "exceptiongroup": {
+            "hashes": [
+                "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9",
+                "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"
+            ],
+            "markers": "python_version < '3.11'",
+            "version": "==1.1.3"
+        },
+        "google-auth": {
+            "hashes": [
+                "sha256:164cba9af4e6e4e40c3a4f90a1a6c12ee56f14c0b4868d1ca91b32826ab334ce",
+                "sha256:d61d1b40897407b574da67da1a833bdc10d5a11642566e506565d1b1a46ba873"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==2.22.0"
+        },
+        "h11": {
+            "hashes": [
+                "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d",
+                "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==0.14.0"
+        },
+        "idna": {
+            "hashes": [
+                "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
+                "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"
+            ],
+            "markers": "python_version >= '3.5'",
+            "version": "==3.4"
+        },
+        "iniconfig": {
+            "hashes": [
+                "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3",
+                "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==2.0.0"
+        },
+        "kubernetes": {
+            "hashes": [
+                "sha256:0f9376329c85cf07615ed6886bf9bf21eb1cbfc05e14ec7b0f74ed8153cd2815",
+                "sha256:d479931c6f37561dbfdf28fc5f46384b1cb8b28f9db344ed4a232ce91990825a"
+            ],
+            "index": "pypi",
+            "version": "==27.2.0"
+        },
+        "oauthlib": {
+            "hashes": [
+                "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca",
+                "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==3.2.2"
+        },
+        "outcome": {
+            "hashes": [
+                "sha256:6f82bd3de45da303cf1f771ecafa1633750a358436a8bb60e06a1ceb745d2672",
+                "sha256:c4ab89a56575d6d38a05aa16daeaa333109c1f96167aba8901ab18b6b5e0f7f5"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==1.2.0"
+        },
+        "packaging": {
+            "hashes": [
+                "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61",
+                "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==23.1"
+        },
+        "passlib": {
+            "hashes": [
+                "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1",
+                "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"
+            ],
+            "index": "pypi",
+            "version": "==1.7.4"
+        },
+        "pluggy": {
+            "hashes": [
+                "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849",
+                "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==1.2.0"
+        },
+        "pyasn1": {
+            "hashes": [
+                "sha256:87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57",
+                "sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
+            "version": "==0.5.0"
+        },
+        "pyasn1-modules": {
+            "hashes": [
+                "sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c",
+                "sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
+            "version": "==0.3.0"
+        },
+        "pycparser": {
+            "hashes": [
+                "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9",
+                "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"
+            ],
+            "version": "==2.21"
+        },
+        "pygit2": {
+            "hashes": [
+                "sha256:14ae27491347a0ac4bbe8347b09d752cfe7fea1121c14525415e0cca6db4a836",
+                "sha256:214bd214784fcbef7a8494d1d59e0cd3a731c0d24ce0f230dcc843322ee33b08",
+                "sha256:22e7f3ad2b7b0c80be991bb47d8a2f2535cc9bf090746eb8679231ee565fde81",
+                "sha256:25a6548930328c5247bfb7c67d29104e63b036cb5390f032d9f91f63efb70434",
+                "sha256:336c864ac961e7be8ba06e9ed8c999e4f624a8ccd90121cc4e40956d8b57acac",
+                "sha256:546091316c9a8c37b9867ddcc6c9f7402ca4d0b9db3f349212a7b5e71988e359",
+                "sha256:56e85d0e66de957d599d1efb2409d39afeefd8f01009bfda0796b42a4b678358",
+                "sha256:5b3ab4d6302990f7adb2b015bcbda1f0715277008d0c66440497e6f8313bf9cb",
+                "sha256:5c1e26649e1540b6a774f812e2fc9890320ff4d33f16db1bb02626318b5ceae2",
+                "sha256:5f65483ab5e3563c58f60debe2acc0979fdf6fd633432fcfbddf727a9a265ba4",
+                "sha256:685378852ef8eb081333bc80dbdfc4f1333cf4a8f3baf614c4135e02ad1ee38a",
+                "sha256:6a4083ba093c69142e0400114a4ef75e87834637d2bbfd77b964614bf70f624f",
+                "sha256:79fbd99d3e08ca7478150eeba28ca4d4103f564148eab8d00aba8f1e6fc60654",
+                "sha256:7bb30ab1fdaa4c30821fed33892958b6d92d50dbd03c76f7775b4e5d62f53a2e",
+                "sha256:857c5cde635d470f58803d67bfb281dc4f6336065a0253bfbed001f18e2d0767",
+                "sha256:8bf14196cbfffbcd286f459a1d4fc660c5d5dfa8fb422e21216961df575410d6",
+                "sha256:8da8517809635ea3da950d9cf99c6d1851352d92b6db309382db88a01c3b0bfd",
+                "sha256:8f443d3641762b2bb9c76400bb18beb4ba27dd35bc098a8bfae82e6a190c52ab",
+                "sha256:926f2e48c4eaa179249d417b8382290b86b0f01dbf41d289f763576209276b9f",
+                "sha256:a365ffca23d910381749fdbcc367db52fe808f9aa4852914dd9ef8b711384a32",
+                "sha256:ac2b5f408eb882e79645ebb43039ac37739c3edd25d857cc97d7482a684b613f",
+                "sha256:b9c2359b99eed8e7fac30c06e6b4ae277a6a0537d6b4b88a190828c3d7eb9ef2",
+                "sha256:be3bb0139f464947523022a5af343a2e862c4ff250a57ec9f631449e7c0ba7c0",
+                "sha256:c74e7601cb8b8dc3d02fd32274e200a7761cffd20ee531442bf1fa115c8f99a5",
+                "sha256:cdf655e5f801990f5cad721b6ccbe7610962f0a4f1c20373dbf9c0be39374a81",
+                "sha256:e7e705aaecad85b883022e81e054fbd27d26023fc031618ee61c51516580517e",
+                "sha256:ec04c27be5d5af1ceecdcc0464e07081222f91f285f156dc53b23751d146569a",
+                "sha256:f4df3e5745fdf3111a6ccc905eae99f22f1a180728f714795138ca540cc2a50a",
+                "sha256:f8f813d35d836c5b0d1962c387754786bcc7f1c3c8e11207b9eeb30238ac4cc7",
+                "sha256:fb9eb57b75ce586928053692a25aae2a50fef3ad36661c57c07d4902899b1df3",
+                "sha256:fe35a72af61961dbb7fb4abcdaa36d5f1c85b2cd3daae94137eeb9c07215cdd3"
+            ],
+            "index": "pypi",
+            "version": "==1.12.2"
+        },
+        "pyopenssl": {
+            "hashes": [
+                "sha256:24f0dc5227396b3e831f4c7f602b950a5e9833d292c8e4a2e06b709292806ae2",
+                "sha256:276f931f55a452e7dea69c7173e984eb2a4407ce413c918aa34b55f82f9b8bac"
+            ],
+            "index": "pypi",
+            "version": "==23.2.0"
+        },
+        "pysocks": {
+            "hashes": [
+                "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299",
+                "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5",
+                "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"
+            ],
+            "version": "==1.7.1"
+        },
+        "pytest": {
+            "hashes": [
+                "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32",
+                "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"
+            ],
+            "index": "pypi",
+            "version": "==7.4.0"
+        },
+        "pytest-timeout": {
+            "hashes": [
+                "sha256:c07ca07404c612f8abbe22294b23c368e2e5104b521c1790195561f37e1ac3d9",
+                "sha256:f6f50101443ce70ad325ceb4473c4255e9d74e3c7cd0ef827309dfa4c0d975c6"
+            ],
+            "index": "pypi",
+            "version": "==2.1.0"
+        },
+        "python-dateutil": {
+            "hashes": [
+                "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
+                "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==2.8.2"
+        },
+        "pyyaml": {
+            "hashes": [
+                "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc",
+                "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741",
+                "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206",
+                "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27",
+                "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595",
+                "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62",
+                "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98",
+                "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696",
+                "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d",
+                "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867",
+                "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47",
+                "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486",
+                "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6",
+                "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3",
+                "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007",
+                "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938",
+                "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c",
+                "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735",
+                "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d",
+                "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba",
+                "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8",
+                "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5",
+                "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd",
+                "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3",
+                "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0",
+                "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515",
+                "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c",
+                "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c",
+                "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924",
+                "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34",
+                "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43",
+                "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859",
+                "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673",
+                "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a",
+                "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab",
+                "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa",
+                "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c",
+                "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585",
+                "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d",
+                "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==6.0.1"
+        },
+        "requests": {
+            "hashes": [
+                "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f",
+                "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"
+            ],
+            "index": "pypi",
+            "version": "==2.31.0"
+        },
+        "requests-oauthlib": {
+            "hashes": [
+                "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5",
+                "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==1.3.1"
+        },
+        "rsa": {
+            "hashes": [
+                "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7",
+                "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"
+            ],
+            "markers": "python_version >= '3.6' and python_version < '4'",
+            "version": "==4.9"
+        },
+        "selenium": {
+            "hashes": [
+                "sha256:98e72117b194b3fa9c69b48998f44bf7dd4152c7bd98544911a1753b9f03cc7d",
+                "sha256:9f9a5ed586280a3594f7461eb1d9dab3eac9d91e28572f365e9b98d9d03e02b5"
+            ],
+            "index": "pypi",
+            "version": "==4.11.2"
+        },
+        "six": {
+            "hashes": [
+                "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
+                "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==1.16.0"
+        },
+        "sniffio": {
+            "hashes": [
+                "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101",
+                "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==1.3.0"
+        },
+        "sortedcontainers": {
+            "hashes": [
+                "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88",
+                "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"
+            ],
+            "version": "==2.4.0"
+        },
+        "tomli": {
+            "hashes": [
+                "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
+                "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
+            ],
+            "markers": "python_version < '3.11'",
+            "version": "==2.0.1"
+        },
+        "trio": {
+            "hashes": [
+                "sha256:3887cf18c8bcc894433420305468388dac76932e9668afa1c49aa3806b6accb3",
+                "sha256:f43da357620e5872b3d940a2e3589aa251fd3f881b65a608d742e00809b1ec38"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==0.22.2"
+        },
+        "trio-websocket": {
+            "hashes": [
+                "sha256:1a748604ad906a7dcab9a43c6eb5681e37de4793ba0847ef0bc9486933ed027b",
+                "sha256:a9937d48e8132ebf833019efde2a52ca82d223a30a7ea3e8d60a7d28f75a4e3a"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==0.10.3"
+        },
+        "urllib3": {
+            "extras": [],
+            "hashes": [
+                "sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f",
+                "sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
+            "version": "==1.26.16"
+        },
+        "websocket-client": {
+            "hashes": [
+                "sha256:c951af98631d24f8df89ab1019fc365f2227c0892f12fd150e935607c79dd0dd",
+                "sha256:f1f9f2ad5291f0225a49efad77abf9e700b6fef553900623060dad6e26503b9d"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==1.6.1"
+        },
+        "wsproto": {
+            "hashes": [
+                "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065",
+                "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"
+            ],
+            "markers": "python_full_version >= '3.7.0'",
+            "version": "==1.2.0"
+        }
+    },
+    "develop": {
+        "astroid": {
+            "hashes": [
+                "sha256:389656ca57b6108f939cf5d2f9a2a825a3be50ba9d589670f393236e0a03b91c",
+                "sha256:903f024859b7c7687d7a7f3a3f73b17301f8e42dfd9cc9df9d4418172d3e2dbd"
+            ],
+            "markers": "python_full_version >= '3.7.2'",
+            "version": "==2.15.6"
+        },
+        "black": {
+            "hashes": [
+                "sha256:01ede61aac8c154b55f35301fac3e730baf0c9cf8120f65a9cd61a81cfb4a0c3",
+                "sha256:022a582720b0d9480ed82576c920a8c1dde97cc38ff11d8d8859b3bd6ca9eedb",
+                "sha256:25cc308838fe71f7065df53aedd20327969d05671bac95b38fdf37ebe70ac087",
+                "sha256:27eb7a0c71604d5de083757fbdb245b1a4fae60e9596514c6ec497eb63f95320",
+                "sha256:327a8c2550ddc573b51e2c352adb88143464bb9d92c10416feb86b0f5aee5ff6",
+                "sha256:47e56d83aad53ca140da0af87678fb38e44fd6bc0af71eebab2d1f59b1acf1d3",
+                "sha256:501387a9edcb75d7ae8a4412bb8749900386eaef258f1aefab18adddea1936bc",
+                "sha256:552513d5cd5694590d7ef6f46e1767a4df9af168d449ff767b13b084c020e63f",
+                "sha256:5c4bc552ab52f6c1c506ccae05681fab58c3f72d59ae6e6639e8885e94fe2587",
+                "sha256:642496b675095d423f9b8448243336f8ec71c9d4d57ec17bf795b67f08132a91",
+                "sha256:6d1c6022b86f83b632d06f2b02774134def5d4d4f1dac8bef16d90cda18ba28a",
+                "sha256:7f3bf2dec7d541b4619b8ce526bda74a6b0bffc480a163fed32eb8b3c9aed8ad",
+                "sha256:831d8f54c3a8c8cf55f64d0422ee875eecac26f5f649fb6c1df65316b67c8926",
+                "sha256:8417dbd2f57b5701492cd46edcecc4f9208dc75529bcf76c514864e48da867d9",
+                "sha256:86cee259349b4448adb4ef9b204bb4467aae74a386bce85d56ba4f5dc0da27be",
+                "sha256:893695a76b140881531062d48476ebe4a48f5d1e9388177e175d76234ca247cd",
+                "sha256:9fd59d418c60c0348505f2ddf9609c1e1de8e7493eab96198fc89d9f865e7a96",
+                "sha256:ad0014efc7acf0bd745792bd0d8857413652979200ab924fbf239062adc12491",
+                "sha256:b5b0ee6d96b345a8b420100b7d71ebfdd19fab5e8301aff48ec270042cd40ac2",
+                "sha256:c333286dc3ddca6fdff74670b911cccedacb4ef0a60b34e491b8a67c833b343a",
+                "sha256:f9062af71c59c004cd519e2fb8f5d25d39e46d3af011b41ab43b9c74e27e236f",
+                "sha256:fb074d8b213749fa1d077d630db0d5f8cc3b2ae63587ad4116e8a436e9bbe995"
+            ],
+            "index": "pypi",
+            "version": "==23.7.0"
+        },
+        "click": {
+            "hashes": [
+                "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd",
+                "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==8.1.6"
+        },
+        "dill": {
+            "hashes": [
+                "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e",
+                "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"
+            ],
+            "markers": "python_version < '3.11'",
+            "version": "==0.3.7"
+        },
+        "isort": {
+            "hashes": [
+                "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504",
+                "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"
+            ],
+            "markers": "python_full_version >= '3.8.0'",
+            "version": "==5.12.0"
+        },
+        "lazy-object-proxy": {
+            "hashes": [
+                "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382",
+                "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82",
+                "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9",
+                "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494",
+                "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46",
+                "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30",
+                "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63",
+                "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4",
+                "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae",
+                "sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be",
+                "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701",
+                "sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd",
+                "sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006",
+                "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a",
+                "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586",
+                "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8",
+                "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821",
+                "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07",
+                "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b",
+                "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171",
+                "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b",
+                "sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2",
+                "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7",
+                "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4",
+                "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8",
+                "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e",
+                "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f",
+                "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda",
+                "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4",
+                "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e",
+                "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671",
+                "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11",
+                "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455",
+                "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734",
+                "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb",
+                "sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==1.9.0"
+        },
+        "mccabe": {
+            "hashes": [
+                "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325",
+                "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==0.7.0"
+        },
+        "mypy-extensions": {
+            "hashes": [
+                "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d",
+                "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"
+            ],
+            "markers": "python_version >= '3.5'",
+            "version": "==1.0.0"
+        },
+        "packaging": {
+            "hashes": [
+                "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61",
+                "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==23.1"
+        },
+        "pathspec": {
+            "hashes": [
+                "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20",
+                "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==0.11.2"
+        },
+        "platformdirs": {
+            "hashes": [
+                "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d",
+                "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==3.10.0"
+        },
+        "pylint": {
+            "hashes": [
+                "sha256:73995fb8216d3bed149c8d51bba25b2c52a8251a2c8ac846ec668ce38fab5413",
+                "sha256:f7b601cbc06fef7e62a754e2b41294c2aa31f1cb659624b9a85bcba29eaf8252"
+            ],
+            "index": "pypi",
+            "version": "==2.17.5"
+        },
+        "tomli": {
+            "hashes": [
+                "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
+                "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
+            ],
+            "markers": "python_version < '3.11'",
+            "version": "==2.0.1"
+        },
+        "tomlkit": {
+            "hashes": [
+                "sha256:38e1ff8edb991273ec9f6181244a6a391ac30e9f5098e7535640ea6be97a7c86",
+                "sha256:712cbd236609acc6a3e2e97253dfc52d4c2082982a88f61b640ecf0817eab899"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==0.12.1"
+        },
+        "typing-extensions": {
+            "hashes": [
+                "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36",
+                "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"
+            ],
+            "markers": "python_version < '3.10'",
+            "version": "==4.7.1"
+        },
+        "wrapt": {
+            "hashes": [
+                "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0",
+                "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420",
+                "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a",
+                "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c",
+                "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079",
+                "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923",
+                "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f",
+                "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1",
+                "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8",
+                "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86",
+                "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0",
+                "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364",
+                "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e",
+                "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c",
+                "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e",
+                "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c",
+                "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727",
+                "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff",
+                "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e",
+                "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29",
+                "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7",
+                "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72",
+                "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475",
+                "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a",
+                "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317",
+                "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2",
+                "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd",
+                "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640",
+                "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98",
+                "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248",
+                "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e",
+                "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d",
+                "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec",
+                "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1",
+                "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e",
+                "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9",
+                "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92",
+                "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb",
+                "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094",
+                "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46",
+                "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29",
+                "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd",
+                "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705",
+                "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8",
+                "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975",
+                "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb",
+                "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e",
+                "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b",
+                "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418",
+                "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019",
+                "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1",
+                "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba",
+                "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6",
+                "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2",
+                "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3",
+                "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7",
+                "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752",
+                "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416",
+                "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f",
+                "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1",
+                "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc",
+                "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145",
+                "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee",
+                "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a",
+                "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7",
+                "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b",
+                "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653",
+                "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0",
+                "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90",
+                "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29",
+                "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6",
+                "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034",
+                "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09",
+                "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559",
+                "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"
+            ],
+            "markers": "python_version < '3.11'",
+            "version": "==1.15.0"
+        }
+    }
+}
diff --git a/charts/k8s-gerrit/README.md b/charts/k8s-gerrit/README.md
new file mode 100644
index 0000000..5c16b2e
--- /dev/null
+++ b/charts/k8s-gerrit/README.md
@@ -0,0 +1,292 @@
+# Gerrit Deployment on Kubernetes
+
+Container images, configurations, [helm charts](https://helm.sh/) and a Kubernetes
+Operator for installing [Gerrit](https://www.gerritcodereview.com/) on
+[Kubernetes](https://kubernetes.io/).
+
+# Deploying Gerrit on Kubernetes
+
+This project provides helm-charts to install Gerrit either as a primary instance
+or a replica on Kubernetes.
+
+The helm-charts are located in the `./helm-charts`-directory. Currently, the
+charts are not published in a registry and have to be deployed from local sources.
+
+For a detailed guide of how to install the helm-charts refer to the respective
+READMEs in the helm-charts directories:
+
+- [gerrit](helm-charts/gerrit/README.md)
+- [gerrit-replica](helm-charts/gerrit-replica/README.md)
+
+These READMEs detail the prerequisites required by the charts as well as all
+configuration options currently provided by the charts.
+
+To evaluate and test the helm-charts, they can be installed on a local machine
+running Minikube. Follow this [guide](Documentation/minikube.md) to get a detailed
+description how to set up the Minikube cluster and install the charts.
+
+Alternatively, a Gerrit Operator can be used to install and operate Gerrit in a
+Kubernetes cluster. The [documentation](./Documentation/operator.md) describes
+how to build and deploy the Gerrit Operator and how to use it to install Gerrit.
+
+# Docker images
+
+This project provides the sources for docker images used by the helm-charts.
+The images are also provided on [Dockerhub](https://hub.docker.com/u/k8sgerrit).
+
+The project also provides scripts to build and publish the images so that custom
+versions can be used by the helm-charts. This requires however a docker registry
+that can be accessed from the Kubernetes cluster, on which Gerrit will be
+deployed. The functionality of the scripts is described in the following sub-
+sections.
+
+## Building images
+
+To build all images, the `build`-script in the root directory of the project can
+be used:
+
+```
+./build
+```
+
+If a specific image should be built, the image name can be specified as an argument.
+Multiple images can be specified at once:
+
+```
+./build gerrit git-gc
+```
+
+The build-script usually uses the `latest`-tag to tag the images. By using the
+`--tag TAG`-option, a custom tag can be defined:
+
+```
+./build --tag test
+```
+
+The version of Gerrit built into the images can be changed by providing a download
+URL for a `.war`-file containing Gerrit:
+
+```
+./build --gerrit-url https://example.com/gerrit.war
+```
+
+The version of a health-check plugin built into the images can be changed by
+providing a download URL for a `.jar`-file containing the plugin:
+
+```
+./build --healthcheck-jar-url https://example.com/healthcheck.jar
+```
+
+The build script will in addition tag the image with the output of
+`git describe --dirty`.
+
+The single component images inherit a base image. The `Dockerfile` for the base
+image can be found in the `./base`-directory. It will be
+automatically built by the `./build`-script. If the component images are built
+manually, the base image has to be built first with the target
+`base:latest`, since it is not available in a registry and thus has
+to exist locally.
+
+## Publishing images
+
+The publish script in the root directory of the project can be used to push the
+built images to the configured registry. To do so, log in first, before executing
+the script.
+
+```
+docker login <registry>
+```
+
+To configure the registry and image version, the respective values can be
+configured via env variables `REGISTRY` and `TAG`. In addition, these values can
+also be passed as command line options named `--registry` and `--tag` in which
+case they override the values from env variables:
+
+```
+./publish <component-name>
+```
+
+The `<component-name>` is one of: `apache-git-http-backend`, `git-gc`, `gerrit`
+or `gerrit-init`.
+
+Adding the `--update-latest`-flag will also update the images tagged `latest` in
+the repository:
+
+```
+./publish --update-latest <component-name>
+```
+
+## Running images in Docker
+
+The container images are meant to be used by the helm-charts provided in this
+project. The images are thus not designed to be used in a standalone setup. To
+run Gerrit on Docker use the
+[docker-gerrit](https://gerrit-review.googlesource.com/admin/repos/docker-gerrit)
+project.
+
+# Running tests
+
+The tests are implemented using Python and `pytest`. To ensure a well-defined
+test-environment, `pipenv` is meant to be used to install packages and provide a
+virtual environment in which to run the tests. To install pipenv, use `brew`:
+
+```sh
+brew install pipenv
+```
+
+More detailed information can be found in the
+[pipenv GitHub repo](https://github.com/pypa/pipenv).
+
+To create the virtual environment with all required packages, run:
+
+```sh
+pipenv install
+```
+
+To run all tests, execute:
+
+```sh
+pipenv run pytest -m "not smoke"
+```
+
+***note
+The `-m "not smoke"`-option excludes the smoke tests, which will fail, since
+no Gerrit-instance will be running, when they are executed.
+***
+
+Some tests will need to create files in a temporary directory. Some of these
+files will be mounted into docker containers by tests. For this to work make
+either sure that the system temporary directory is accessible by the Docker
+daemon or set the base temporary directory to a directory accessible by Docker
+by executing:
+
+```sh
+pipenv run pytest --basetemp=/tmp/k8sgerrit -m "not smoke"
+```
+
+By default the tests will build all images from scratch. This will greatly
+increase the time needed for testing. To use already existing container images,
+a tag can be provided as follows:
+
+```sh
+pipenv run pytest --tag=v0.1 -m "not smoke"
+```
+
+The tests will then use the existing images with the provided tag. If an image
+does not exist, it will still be built by the tests.
+
+By default the build of the container images will not use the build cache
+created by docker. To enable the cache, execute:
+
+```sh
+pipenv run pytest --build-cache -m "not smoke"
+```
+
+Slow tests may be marked with the decorator `@pytest.mark.slow`. These tests
+may then be skipped as follows:
+
+```sh
+pipenv run pytest --skip-slow -m "not smoke"
+```
+
+There are also other marks, allowing to select tests (refer to
+[this section](#test-marks)).
+
+To run specific tests, execute one of the following:
+
+```sh
+# Run all tests in a directory (including subdirectories)
+pipenv run pytest tests/container-images/base
+
+# Run all tests in a file
+pipenv run pytest tests/container-images/base/test_container_build_base.py
+
+# Run a specific test
+pipenv run \
+  pytest tests/container-images/base/test_container_build_base.py::test_build_base
+
+# Run tests with a specific marker
+pipenv run pytest -m "docker"
+```
+
+For a more detailed description of how to use `pytest`, refer to the
+[official documentation](https://docs.pytest.org/en/latest/contents.html).
+
+## Test marks
+
+### docker
+
+Marks tests which start up docker containers. These tests will interact with
+the containers by either using `docker exec` or sending HTTP-requests. Make
+sure that your system supports this kind of interaction.
+
+### incremental
+
+Marks test classes in which the contained test functions have to run
+incrementally.
+
+### integration
+
+Marks integration tests. These tests test interactions between containers,
+between outside clients and containers and between the components installed
+by a helm chart.
+
+### kubernetes
+
+Marks tests that require a Kubernetes cluster. These tests are used to test the
+functionality of the helm charts in this project and the interaction of the
+components installed by them. The cluster should not be used for other purposes
+to minimize unforeseen interactions.
+
+These tests require a storage class with ReadWriteMany access mode within the
+cluster. The name of the storage class has to be provided with the
+`--rwm-storageclass`-option (default: `shared-storage`).
+
+### slow
+
+Marks tests that need an above average time to run.
+
+### structure
+
+Marks structure tests. These tests are meant to test, whether certain components
+exist in a container. These tests ensure that components expected by the users
+of the container, e.g. the helm charts, are present in the containers.
+
+## Running smoke tests
+
+To run smoke tests, use the following command:
+
+```sh
+pipenv run pytest \
+  -m "smoke" \
+  --basetemp="<tmp-dir for tests>" \
+  --ingress-url="<Gerrit URL>" \
+  --gerrit-user="<Gerrit user>" \
+  --gerrit-pwd
+```
+
+The smoke tests require a Gerrit user that is allowed to create and delete
+projects. The username has to be given by `--gerit-user`. Setting the
+`--gerrit-pwd`-flag will cause a password prompt to enter the password of the
+Gerrit-user.
+
+# Contributing
+
+Contributions to this project are welcome. If you are new to the Gerrit workflow,
+refer to the [Gerrit-documentation](https://gerrit-review.googlesource.com/Documentation/intro-gerrit-walkthrough.html)
+for guidance on how to contribute changes.
+
+The contribution guidelines for this project can be found
+[here](Documentation/developer-guide.md).
+
+# Roadmap
+
+The roadmap of this project can be found [here](Documentation/roadmap.md).
+
+Feature requests can be made by pushing a change for the roadmap. This can also
+be done to announce/discuss features that you would like to provide.
+
+# Contact
+
+The [Gerrit Mailing List](https://groups.google.com/forum/#!forum/repo-discuss)
+can be used to post questions and comments on this project or Gerrit in general.
diff --git a/charts/k8s-gerrit/build b/charts/k8s-gerrit/build
new file mode 100755
index 0000000..aa8de36
--- /dev/null
+++ b/charts/k8s-gerrit/build
@@ -0,0 +1,128 @@
+#!/bin/bash
+
+usage() {
+    me=`basename "$0"`
+    echo >&2 "Usage: $me [--help] [--tag TAG] [--gerrit-url URL] [--base-image IMAGE] [IMAGE]"
+    exit 1
+}
+
+while test $# -gt 0 ; do
+  case "$1" in
+  --help)
+    usage
+    ;;
+
+  --tag)
+    shift
+    TAG=$1
+    shift
+    ;;
+
+  --gerrit-url)
+    shift
+    GERRIT_WAR_URL=$1
+    shift
+    ;;
+
+  --healthcheck-jar-url)
+    shift
+    HEALTHCHECK_JAR_URL=$1
+    shift
+    ;;
+
+  --base-image)
+    shift
+    BASE_IMAGE=$1
+    shift
+    ;;
+
+  *)
+    break
+  esac
+done
+
+#Get list of images
+source container-images/publish_list
+IMAGES=$(get_image_list)
+
+if test -n "$GERRIT_WAR_URL"; then
+    BUILD_ARGS="--build-arg GERRIT_WAR_URL=$GERRIT_WAR_URL"
+fi
+
+if test -n "$HEALTHCHECK_JAR_URL"; then
+    BUILD_ARGS="$BUILD_ARGS --build-arg HEALTHCHECK_JAR_URL=$HEALTHCHECK_JAR_URL"
+fi
+
+export REV="$(git describe --always --dirty)"
+
+docker_build(){
+    IMAGE=$1
+
+    docker build \
+        --platform=linux/amd64 \
+        --build-arg TAG=$REV \
+        -t k8sgerrit/$IMAGE:$TAG \
+        ./container-images/$IMAGE
+
+    if test $? -ne 0; then
+        REPORT="$REPORT Failed: k8sgerrit/$IMAGE.\n"
+        RETURN_CODE=1
+    else
+        REPORT="$REPORT Success: k8sgerrit/$IMAGE:$TAG\n"
+    fi
+}
+
+docker_build_gerrit_base(){
+    BUILD_ARGS="$BUILD_ARGS --build-arg TAG=$REV"
+    docker build \
+        --platform=linux/amd64 \
+        $BUILD_ARGS \
+        -t gerrit-base:$REV \
+        ./container-images/gerrit-base
+    if test $? -ne 0; then
+        echo -e "\n\nFailed to build gerrit-base image."
+        exit 1
+    fi
+
+    if test -z "$TAG"; then
+        export TAG="$(./get_version.sh)"
+    fi
+}
+
+REPORT="Build results: \n"
+RETURN_CODE=0
+
+if test -n "$BASE_IMAGE"; then
+    BASE_BUILD_ARGS="--build-arg BASE_IMAGE=$BASE_IMAGE"
+fi
+
+docker build $BASE_BUILD_ARGS --platform=linux/amd64 -t base:$REV ./container-images/base
+if test $? -ne 0; then
+    echo -e "\n\nFailed to build base image."
+    exit 1
+fi
+
+if test $# -eq 0 ; then
+    docker_build_gerrit_base
+    for IMAGE in $IMAGES; do
+        docker_build $IMAGE
+    done
+else
+    while test $# -gt 0 ; do
+        if [[ $1 = gerrit-* ]]; then
+            docker_build_gerrit_base
+        else
+            if test -z "$TAG"; then
+                TAG="$(git describe --always --dirty)-unknown"
+            fi
+            echo -e "\nNo Image containing Gerrit will be built." \
+                    "The Gerrit-version can thus not be determinded." \
+                    "Using tag $TAG\n"
+        fi
+        docker_build $1
+        shift
+    done
+fi
+
+echo -e "\n\n$REPORT"
+exit $RETURN_CODE
diff --git a/charts/k8s-gerrit/container-images/apache-git-http-backend/Dockerfile b/charts/k8s-gerrit/container-images/apache-git-http-backend/Dockerfile
new file mode 100644
index 0000000..aa6c6c9
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/apache-git-http-backend/Dockerfile
@@ -0,0 +1,30 @@
+ARG TAG=latest
+FROM base:${TAG}
+
+# Install apache2
+RUN apk update && \
+    apk add --no-cache \
+      apache2 \
+      apache2-ctl \
+      apache2-utils \
+      git-daemon \
+      logrotate && \
+    rm /etc/apache2/conf.d/default.conf && \
+    rm /etc/apache2/conf.d/info.conf
+
+# Configure git-http-backend
+COPY config/git-http-backend.conf /etc/apache2/conf.d/
+COPY config/envvars /usr/sbin/envvars
+COPY config/httpd.conf /etc/apache2/httpd.conf
+COPY config/logrotation /etc/logrotate.d/apache2
+
+COPY tools/start /var/tools/start
+COPY tools/project_admin.sh /var/cgi/project_admin.sh
+
+RUN mkdir -p /var/gerrit/git && \
+    mkdir -p /var/log/apache2 && \
+    chown -R gerrit:users /var/gerrit/git && \
+    chown -R gerrit:users /var/log/apache2
+
+# Start
+ENTRYPOINT ["ash", "/var/tools/start"]
diff --git a/charts/k8s-gerrit/container-images/apache-git-http-backend/README.md b/charts/k8s-gerrit/container-images/apache-git-http-backend/README.md
new file mode 100644
index 0000000..0b7146c
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/apache-git-http-backend/README.md
@@ -0,0 +1,26 @@
+# apache-git-http-backend
+
+The apache-git-http-backend docker image serves as receiver in git replication
+from a Gerrit to a Gerrit replica.
+
+## Content
+
+* base image
+* Apache webserver
+* Apache configurations for http
+* git (via base image) and git-deamon for git-http-backend
+* `tools/project_admin.sh`: cgi script to enable remote creation/deletion/HEAD update
+  of git repositories. Compatible with replication plugin.
+* `tools/start`: start script, configures and starts Apache
+ webserver
+
+## Setup and Configuration
+
+* install Apache webserver, additional Apache tools and git daemon
+* configure Apache
+* install cgi scripts
+* map volumes
+
+## Start
+
+* start Apache git-http backend via start script `/var/tools/start`
diff --git a/charts/k8s-gerrit/container-images/apache-git-http-backend/config/envvars b/charts/k8s-gerrit/container-images/apache-git-http-backend/config/envvars
new file mode 100644
index 0000000..97d9f7e
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/apache-git-http-backend/config/envvars
@@ -0,0 +1,43 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+#
+# envvars-std - default environment variables for apachectl
+#
+# This file is generated from envvars-std.in
+#
+if test "x$LD_LIBRARY_PATH" != "x" ; then
+  LD_LIBRARY_PATH="/usr/lib:$LD_LIBRARY_PATH"
+else
+  LD_LIBRARY_PATH="/usr/lib"
+fi
+export LD_LIBRARY_PATH
+#
+
+# this won't be correct after changing uid
+unset HOME
+
+# Since there is no sane way to get the parsed apache2 config in scripts, some
+# settings are defined via environment variables and then used in apache2ctl,
+# /etc/init.d/apache2, /etc/logrotate.d/apache2, etc.
+export APACHE_RUN_USER=gerrit
+export APACHE_RUN_GROUP=users
+# Only /var/log/apache2 is handled by /etc/logrotate.d/apache2.
+export APACHE_LOG_DIR=/var/log/apache2$SUFFIX
+
+## Uncomment the following line to use the system default locale instead:
+#. /etc/default/locale
+
+export LANG
diff --git a/charts/k8s-gerrit/container-images/apache-git-http-backend/config/git-http-backend.conf b/charts/k8s-gerrit/container-images/apache-git-http-backend/config/git-http-backend.conf
new file mode 100644
index 0000000..8967bb1
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/apache-git-http-backend/config/git-http-backend.conf
@@ -0,0 +1,58 @@
+<VirtualHost *:80>
+  # The ServerName directive sets the request scheme, hostname and port that
+  # the server uses to identify itself. This is used when creating
+  # redirection URLs. In the context of virtual hosts, the ServerName
+  # specifies what hostname must appear in the request's Host: header to
+  # match this virtual host. For the default virtual host (this file) this
+  # value is not decisive as it is used as a last resort host regardless.
+  # However, you must set it for any further virtual host explicitly.
+  ServerName localhost
+  ServerAdmin webmaster@localhost
+
+  UseCanonicalName On
+
+  AllowEncodedSlashes On
+
+  SetEnv GIT_PROJECT_ROOT /var/gerrit/git
+  SetEnv GIT_HTTP_EXPORT_ALL
+  ScriptAliasMatch "(?i)^/a/projects/(.*)" "/var/cgi/project_admin.sh"
+  ScriptAlias / /usr/libexec/git-core/git-http-backend/
+  ScriptLog logs/cgi.log
+
+  # Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
+  # error, crit, alert, emerg.
+  # It is also possible to configure the loglevel for particular
+  # modules, e.g.
+  LogLevel debug authz_core:warn
+
+  # Don't log probe requests performed by kubernetes
+  SetEnvIFNoCase User-Agent "^kube-probe" dontlog
+
+  ErrorLog ${APACHE_LOG_DIR}/error.log
+  CustomLog ${APACHE_LOG_DIR}/access.log combined env=!dontlog
+
+  # For most configuration files from conf-available/, which are
+  # enabled or disabled at a global level, it is possible to
+  # include a line for only one particular virtual host. For example the
+  # following line enables the CGI configuration for this host only
+  # after it has been globally disabled with "a2disconf".
+  #Include conf-available/serve-cgi-bin.conf
+  <Files "git-http-backend">
+    AuthType Basic
+    AuthName "Restricted Content"
+    AuthUserFile /var/apache/credentials/.htpasswd
+    Require valid-user
+  </Files>
+  <Files "create_repo.sh">
+    AuthType Basic
+    AuthName "Restricted Content"
+    AuthUserFile /var/apache/credentials/.htpasswd
+    Require valid-user
+  </Files>
+  <Files "project_admin.sh">
+    AuthType Basic
+    AuthName "Restricted Content"
+    AuthUserFile /var/apache/credentials/.htpasswd
+    Require valid-user
+  </Files>
+</VirtualHost>
diff --git a/charts/k8s-gerrit/container-images/apache-git-http-backend/config/httpd.conf b/charts/k8s-gerrit/container-images/apache-git-http-backend/config/httpd.conf
new file mode 100644
index 0000000..7a28460
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/apache-git-http-backend/config/httpd.conf
@@ -0,0 +1,510 @@
+# This is the main Apache HTTP server configuration file.  It contains the
+# configuration directives that give the server its instructions.
+# See <URL:http://httpd.apache.org/docs/2.4/> for detailed information.
+# In particular, see
+# <URL:http://httpd.apache.org/docs/2.4/mod/directives.html>
+# for a discussion of each configuration directive.
+#
+# Do NOT simply read the instructions in here without understanding
+# what they do.  They're here only as hints or reminders.  If you are unsure
+# consult the online docs. You have been warned.
+#
+# Configuration and logfile names: If the filenames you specify for many
+# of the server's control files begin with "/" (or "drive:/" for Win32), the
+# server will use that explicit path.  If the filenames do *not* begin
+# with "/", the value of ServerRoot is prepended -- so "logs/access_log"
+# with ServerRoot set to "/usr/local/apache2" will be interpreted by the
+# server as "/usr/local/apache2/logs/access_log", whereas "/logs/access_log"
+# will be interpreted as '/logs/access_log'.
+
+#
+# ServerTokens
+# This directive configures what you return as the Server HTTP response
+# Header. The default is 'Full' which sends information about the OS-Type
+# and compiled in modules.
+# Set to one of:  Full | OS | Minor | Minimal | Major | Prod
+# where Full conveys the most information, and Prod the least.
+#
+ServerTokens OS
+
+#
+# ServerRoot: The top of the directory tree under which the server's
+# configuration, error, and log files are kept.
+#
+# Do not add a slash at the end of the directory path.  If you point
+# ServerRoot at a non-local disk, be sure to specify a local disk on the
+# Mutex directive, if file-based mutexes are used.  If you wish to share the
+# same ServerRoot for multiple httpd daemons, you will need to change at
+# least PidFile.
+#
+ServerRoot /var/www
+
+#
+# Mutex: Allows you to set the mutex mechanism and mutex file directory
+# for individual mutexes, or change the global defaults
+#
+# Uncomment and change the directory if mutexes are file-based and the default
+# mutex file directory is not on a local disk or is not appropriate for some
+# other reason.
+#
+# Mutex default:/run/apache2
+
+#
+# Listen: Allows you to bind Apache to specific IP addresses and/or
+# ports, instead of the default. See also the <VirtualHost>
+# directive.
+#
+# Change this to Listen on specific IP addresses as shown below to
+# prevent Apache from glomming onto all bound IP addresses.
+#
+#Listen 12.34.56.78:80
+Listen 80
+
+#
+# Dynamic Shared Object (DSO) Support
+#
+# To be able to use the functionality of a module which was built as a DSO you
+# have to place corresponding `LoadModule' lines at this location so the
+# directives contained in it are actually available _before_ they are used.
+# Statically compiled modules (those listed by `httpd -l') do not need
+# to be loaded here.
+#
+# Example:
+# LoadModule foo_module modules/mod_foo.so
+#
+LoadModule mpm_event_module modules/mod_mpm_event.so
+#LoadModule mpm_prefork_module modules/mod_mpm_prefork.so
+#LoadModule mpm_worker_module modules/mod_mpm_worker.so
+LoadModule authn_file_module modules/mod_authn_file.so
+#LoadModule authn_dbm_module modules/mod_authn_dbm.so
+#LoadModule authn_anon_module modules/mod_authn_anon.so
+#LoadModule authn_dbd_module modules/mod_authn_dbd.so
+#LoadModule authn_socache_module modules/mod_authn_socache.so
+LoadModule authn_core_module modules/mod_authn_core.so
+LoadModule authz_host_module modules/mod_authz_host.so
+LoadModule authz_groupfile_module modules/mod_authz_groupfile.so
+LoadModule authz_user_module modules/mod_authz_user.so
+#LoadModule authz_dbm_module modules/mod_authz_dbm.so
+#LoadModule authz_owner_module modules/mod_authz_owner.so
+#LoadModule authz_dbd_module modules/mod_authz_dbd.so
+LoadModule authz_core_module modules/mod_authz_core.so
+LoadModule access_compat_module modules/mod_access_compat.so
+LoadModule auth_basic_module modules/mod_auth_basic.so
+#LoadModule auth_form_module modules/mod_auth_form.so
+#LoadModule auth_digest_module modules/mod_auth_digest.so
+#LoadModule allowmethods_module modules/mod_allowmethods.so
+#LoadModule file_cache_module modules/mod_file_cache.so
+#LoadModule cache_module modules/mod_cache.so
+#LoadModule cache_disk_module modules/mod_cache_disk.so
+#LoadModule cache_socache_module modules/mod_cache_socache.so
+#LoadModule socache_shmcb_module modules/mod_socache_shmcb.so
+#LoadModule socache_dbm_module modules/mod_socache_dbm.so
+#LoadModule socache_memcache_module modules/mod_socache_memcache.so
+#LoadModule socache_redis_module modules/mod_socache_redis.so
+#LoadModule watchdog_module modules/mod_watchdog.so
+#LoadModule macro_module modules/mod_macro.so
+#LoadModule dbd_module modules/mod_dbd.so
+#LoadModule dumpio_module modules/mod_dumpio.so
+#LoadModule echo_module modules/mod_echo.so
+#LoadModule buffer_module modules/mod_buffer.so
+#LoadModule data_module modules/mod_data.so
+#LoadModule ratelimit_module modules/mod_ratelimit.so
+LoadModule reqtimeout_module modules/mod_reqtimeout.so
+#LoadModule ext_filter_module modules/mod_ext_filter.so
+#LoadModule request_module modules/mod_request.so
+#LoadModule include_module modules/mod_include.so
+LoadModule filter_module modules/mod_filter.so
+#LoadModule reflector_module modules/mod_reflector.so
+#LoadModule substitute_module modules/mod_substitute.so
+#LoadModule sed_module modules/mod_sed.so
+#LoadModule charset_lite_module modules/mod_charset_lite.so
+#LoadModule deflate_module modules/mod_deflate.so
+LoadModule mime_module modules/mod_mime.so
+LoadModule log_config_module modules/mod_log_config.so
+#LoadModule log_debug_module modules/mod_log_debug.so
+#LoadModule log_forensic_module modules/mod_log_forensic.so
+#LoadModule logio_module modules/mod_logio.so
+LoadModule env_module modules/mod_env.so
+#LoadModule mime_magic_module modules/mod_mime_magic.so
+#LoadModule expires_module modules/mod_expires.so
+LoadModule headers_module modules/mod_headers.so
+#LoadModule usertrack_module modules/mod_usertrack.so
+#LoadModule unique_id_module modules/mod_unique_id.so
+LoadModule setenvif_module modules/mod_setenvif.so
+LoadModule version_module modules/mod_version.so
+#LoadModule remoteip_module modules/mod_remoteip.so
+#LoadModule session_module modules/mod_session.so
+#LoadModule session_cookie_module modules/mod_session_cookie.so
+#LoadModule session_crypto_module modules/mod_session_crypto.so
+#LoadModule session_dbd_module modules/mod_session_dbd.so
+#LoadModule slotmem_shm_module modules/mod_slotmem_shm.so
+#LoadModule slotmem_plain_module modules/mod_slotmem_plain.so
+#LoadModule dialup_module modules/mod_dialup.so
+#LoadModule http2_module modules/mod_http2.so
+LoadModule unixd_module modules/mod_unixd.so
+#LoadModule heartbeat_module modules/mod_heartbeat.so
+#LoadModule heartmonitor_module modules/mod_heartmonitor.so
+LoadModule status_module modules/mod_status.so
+LoadModule autoindex_module modules/mod_autoindex.so
+#LoadModule asis_module modules/mod_asis.so
+#LoadModule info_module modules/mod_info.so
+#LoadModule suexec_module modules/mod_suexec.so
+<IfModule !mpm_prefork_module>
+	LoadModule cgid_module modules/mod_cgid.so
+</IfModule>
+<IfModule mpm_prefork_module>
+	LoadModule cgi_module modules/mod_cgi.so
+</IfModule>
+#LoadModule vhost_alias_module modules/mod_vhost_alias.so
+#LoadModule negotiation_module modules/mod_negotiation.so
+LoadModule dir_module modules/mod_dir.so
+#LoadModule actions_module modules/mod_actions.so
+#LoadModule speling_module modules/mod_speling.so
+#LoadModule userdir_module modules/mod_userdir.so
+LoadModule alias_module modules/mod_alias.so
+#LoadModule rewrite_module modules/mod_rewrite.so
+LoadModule info_module modules/mod_info.so
+
+LoadModule negotiation_module modules/mod_negotiation.so
+
+<IfModule unixd_module>
+#
+# If you wish httpd to run as a different user or group, you must run
+# httpd as root initially and it will switch.
+#
+# User/Group: The name (or #number) of the user/group to run httpd as.
+# It is usually good practice to create a dedicated user and group for
+# running httpd, as with most system services.
+#
+User ${APACHE_RUN_USER}
+Group ${APACHE_RUN_GROUP}
+
+#
+# Timeout defines, in seconds, the amount of time that the server waits for
+# receipts and transmissions during communications. Timeout is set to 300
+# seconds by default, which is appropriate for most situations.
+#
+Timeout 300
+
+</IfModule>
+
+# 'Main' server configuration
+#
+# The directives in this section set up the values used by the 'main'
+# server, which responds to any requests that aren't handled by a
+# <VirtualHost> definition.  These values also provide defaults for
+# any <VirtualHost> containers you may define later in the file.
+#
+# All of these directives may appear inside <VirtualHost> containers,
+# in which case these default settings will be overridden for the
+# virtual host being defined.
+#
+
+#
+# ServerAdmin: Your address, where problems with the server should be
+# e-mailed.  This address appears on some server-generated pages, such
+# as error documents.  e.g. admin@your-domain.com
+#
+# ServerAdmin you@example.com
+
+#
+# Optionally add a line containing the server version and virtual host
+# name to server-generated pages (internal error documents, FTP directory
+# listings, mod_status and mod_info output etc., but not CGI generated
+# documents or custom error documents).
+# Set to "EMail" to also include a mailto: link to the ServerAdmin.
+# Set to one of:  On | Off | EMail
+#
+ServerSignature On
+
+#
+# ServerName gives the name and port that the server uses to identify itself.
+# This can often be determined automatically, but we recommend you specify
+# it explicitly to prevent problems during startup.
+#
+# If your host doesn't have a registered DNS name, enter its IP address here.
+#
+#ServerName www.example.com:80
+
+#
+# Deny access to the entirety of your server's filesystem. You must
+# explicitly permit access to web content directories in other
+# <Directory> blocks below.
+#
+<Directory />
+    AllowOverride none
+    Require all denied
+</Directory>
+
+#
+# Note that from this point forward you must specifically allow
+# particular features to be enabled - so if something's not working as
+# you might expect, make sure that you have specifically enabled it
+# below.
+#
+
+#
+# DocumentRoot: The directory out of which you will serve your
+# documents. By default, all requests are taken from this directory, but
+# symbolic links and aliases may be used to point to other locations.
+#
+DocumentRoot "/var/www/localhost/htdocs"
+<Directory "/var/www/localhost/htdocs">
+    #
+    # Possible values for the Options directive are "None", "All",
+    # or any combination of:
+    #   Indexes Includes FollowSymLinks SymLinksifOwnerMatch ExecCGI MultiViews
+    #
+    # Note that "MultiViews" must be named *explicitly* --- "Options All"
+    # doesn't give it to you.
+    #
+    # The Options directive is both complicated and important.  Please see
+    # http://httpd.apache.org/docs/2.4/mod/core.html#options
+    # for more information.
+    #
+    Options Indexes FollowSymLinks ExecCGI
+
+    #
+    # AllowOverride controls what directives may be placed in .htaccess files.
+    # It can be "All", "None", or any combination of the keywords:
+    #   AllowOverride FileInfo AuthConfig Limit
+    #
+    AllowOverride None
+
+    #
+    # Controls who can get stuff from this server.
+    #
+    Require all granted
+</Directory>
+
+#
+# DirectoryIndex: sets the file that Apache will serve if a directory
+# is requested.
+#
+<IfModule dir_module>
+    DirectoryIndex index.html
+</IfModule>
+
+#
+# The following lines prevent .htaccess and .htpasswd files from being
+# viewed by Web clients.
+#
+<Files ".ht*">
+    Require all denied
+</Files>
+
+#
+# ErrorLog: The location of the error log file.
+# If you do not specify an ErrorLog directive within a <VirtualHost>
+# container, error messages relating to that virtual host will be
+# logged here.  If you *do* define an error logfile for a <VirtualHost>
+# container, that host's errors will be logged there and not here.
+#
+ErrorLog ${APACHE_LOG_DIR}/error.log
+
+#
+# LogLevel: Control the number of messages logged to the error_log.
+# Possible values include: debug, info, notice, warn, error, crit,
+# alert, emerg.
+#
+LogLevel debug authz_core:warn
+
+<IfModule log_config_module>
+    #
+    # The following directives define some format nicknames for use with
+    # a CustomLog directive (see below).
+    #
+    LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
+    LogFormat "%h %l %u %t \"%r\" %>s %b" common
+
+    <IfModule logio_module>
+      # You need to enable mod_logio.c to use %I and %O
+      LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio
+    </IfModule>
+
+    #
+    # The location and format of the access logfile (Common Logfile Format).
+    # If you do not define any access logfiles within a <VirtualHost>
+    # container, they will be logged here.  Contrariwise, if you *do*
+    # define per-<VirtualHost> access logfiles, transactions will be
+    # logged therein and *not* in this file.
+    #
+    #CustomLog logs/access.log common
+
+    #
+    # If you prefer a logfile with access, agent, and referer information
+    # (Combined Logfile Format) you can use the following directive.
+    #
+    CustomLog logs/access.log combined
+</IfModule>
+
+<IfModule alias_module>
+    #
+    # Redirect: Allows you to tell clients about documents that used to
+    # exist in your server's namespace, but do not anymore. The client
+    # will make a new request for the document at its new location.
+    # Example:
+    # Redirect permanent /foo http://www.example.com/bar
+
+    #
+    # Alias: Maps web paths into filesystem paths and is used to
+    # access content that does not live under the DocumentRoot.
+    # Example:
+    # Alias /webpath /full/filesystem/path
+    #
+    # If you include a trailing / on /webpath then the server will
+    # require it to be present in the URL.  You will also likely
+    # need to provide a <Directory> section to allow access to
+    # the filesystem path.
+
+    #
+    # ScriptAlias: This controls which directories contain server scripts.
+    # ScriptAliases are essentially the same as Aliases, except that
+    # documents in the target directory are treated as applications and
+    # run by the server when requested rather than as documents sent to the
+    # client.  The same rules about trailing "/" apply to ScriptAlias
+    # directives as to Alias.
+    #
+    ScriptAlias /cgi-bin/ "/var/www/localhost/cgi-bin/"
+
+</IfModule>
+
+<IfModule cgid_module>
+    #
+    # ScriptSock: On threaded servers, designate the path to the UNIX
+    # socket used to communicate with the CGI daemon of mod_cgid.
+    #
+    Scriptsock cgisock
+</IfModule>
+
+#
+# "/var/www/localhost/cgi-bin" should be changed to whatever your ScriptAliased
+# CGI directory exists, if you have that configured.
+#
+<Directory "/var/cgi">
+    AllowOverride None
+    Options None
+    Require all granted
+</Directory>
+
+<IfModule headers_module>
+    #
+    # Avoid passing HTTP_PROXY environment to CGI's on this or any proxied
+    # backend servers which have lingering "httpoxy" defects.
+    # 'Proxy' request header is undefined by the IETF, not listed by IANA
+    #
+    RequestHeader unset Proxy early
+</IfModule>
+
+<IfModule mime_module>
+    #
+    # TypesConfig points to the file containing the list of mappings from
+    # filename extension to MIME-type.
+    #
+    TypesConfig /etc/apache2/mime.types
+
+    #
+    # AddType allows you to add to or override the MIME configuration
+    # file specified in TypesConfig for specific file types.
+    #
+    #AddType application/x-gzip .tgz
+    #
+    # AddEncoding allows you to have certain browsers uncompress
+    # information on the fly. Note: Not all browsers support this.
+    #
+    #AddEncoding x-compress .Z
+    #AddEncoding x-gzip .gz .tgz
+    #
+    # If the AddEncoding directives above are commented-out, then you
+    # probably should define those extensions to indicate media types:
+    #
+    AddType application/x-compress .Z
+    AddType application/x-gzip .gz .tgz
+
+    #
+    # AddHandler allows you to map certain file extensions to "handlers":
+    # actions unrelated to filetype. These can be either built into the server
+    # or added with the Action directive (see below)
+    #
+    # To use CGI scripts outside of ScriptAliased directories:
+    # (You will also need to add "ExecCGI" to the "Options" directive.)
+    #
+    AddHandler cgi-script .cgi .sh
+
+    # For type maps (negotiated resources):
+    #AddHandler type-map var
+
+    #
+    # Filters allow you to process content before it is sent to the client.
+    #
+    # To parse .shtml files for server-side includes (SSI):
+    # (You will also need to add "Includes" to the "Options" directive.)
+    #
+    #AddType text/html .shtml
+    #AddOutputFilter INCLUDES .shtml
+</IfModule>
+
+<IfModule status_module>
+#
+# Allow server status reports generated by mod_status,
+# with the URL of http://servername/server-status
+# Change the ".example.com" to match your domain to enable.
+
+<Location /server-status>
+    SetHandler server-status
+    Require ip 127.0.0.1
+</Location>
+
+#
+# ExtendedStatus controls whether Apache will generate "full" status
+# information (ExtendedStatus On) or just basic information (ExtendedStatus
+# Off) when the "server-status" handler is called. The default is Off.
+#
+#ExtendedStatus On
+</IfModule>
+
+<IfModule info_module>
+#
+# Allow remote server configuration reports, with the URL of
+#  http://servername/server-info (requires that mod_info.c be loaded).
+# Change the ".example.com" to match your domain to enable.
+#
+<Location /server-info>
+    SetHandler server-info
+    Require ip 127.0.0.1
+</Location>
+</IfModule>
+
+#
+# Customizable error responses come in three flavors:
+# 1) plain text 2) local redirects 3) external redirects
+#
+# Some examples:
+#ErrorDocument 500 "The server made a boo boo."
+#ErrorDocument 404 /missing.html
+#ErrorDocument 404 "/cgi-bin/missing_handler.pl"
+#ErrorDocument 402 http://www.example.com/subscription_info.html
+#
+
+#
+# MaxRanges: Maximum number of Ranges in a request before
+# returning the entire resource, or one of the special
+# values 'default', 'none' or 'unlimited'.
+# Default setting is to accept 200 Ranges.
+#MaxRanges unlimited
+
+#
+# EnableMMAP and EnableSendfile: On systems that support it,
+# memory-mapping or the sendfile syscall may be used to deliver
+# files.  This usually improves server performance, but must
+# be turned off when serving from networked-mounted
+# filesystems or if support for these functions is otherwise
+# broken on your system.
+# Defaults: EnableMMAP On, EnableSendfile Off
+#
+#EnableMMAP off
+#EnableSendfile on
+
+# Load config files from the config directory "/etc/apache2/conf.d".
+#
+IncludeOptional /etc/apache2/conf.d/*.conf
diff --git a/charts/k8s-gerrit/container-images/apache-git-http-backend/config/logrotation b/charts/k8s-gerrit/container-images/apache-git-http-backend/config/logrotation
new file mode 100644
index 0000000..282b1e8
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/apache-git-http-backend/config/logrotation
@@ -0,0 +1,12 @@
+/var/log/apache2/*log {
+    daily
+    dateext
+    compress
+    delaycompress
+    missingok
+    notifempty
+    sharedscripts
+    postrotate
+        /etc/init.d/apache2 --quiet --ifstarted reload > /dev/null 2>/dev/null || true
+    endscript
+}
diff --git a/charts/k8s-gerrit/container-images/apache-git-http-backend/tools/project_admin.sh b/charts/k8s-gerrit/container-images/apache-git-http-backend/tools/project_admin.sh
new file mode 100755
index 0000000..7b46ac9
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/apache-git-http-backend/tools/project_admin.sh
@@ -0,0 +1,66 @@
+#!/bin/ash
+
+delete() {
+    rm -rf /var/gerrit/git/${REPO}.git
+
+    if ! test -f /var/gerrit/git/${REPO}.git; then
+        STATUS_CODE="204 No Content"
+        MESSAGE="Repository ${REPO} deleted."
+    else
+        MESSAGE="Repository ${REPO} could not be deleted."
+    fi
+}
+
+new() {
+    if test -d /var/gerrit/git/${REPO}.git; then
+        STATUS_CODE="200 OK"
+        MESSAGE="Repository already available."
+    else
+        git init --bare /var/gerrit/git/${REPO}.git > /dev/null
+        if test -f /var/gerrit/git/${REPO}.git/HEAD; then
+            STATUS_CODE="201 Created"
+            MESSAGE="Repository ${REPO} created."
+        else
+            MESSAGE="Repository ${REPO} could not be created."
+        fi
+    fi
+}
+
+update_head(){
+    read -n ${CONTENT_LENGTH} POST_STRING
+    NEW_HEAD=$(echo ${POST_STRING} | jq .ref - | tr -d '"')
+
+    git --git-dir /var/gerrit/git/${REPO}.git symbolic-ref HEAD ${NEW_HEAD}
+
+    if test "ref: ${NEW_HEAD}" == "$(cat /var/gerrit/git/${REPO}.git/HEAD)"; then
+        STATUS_CODE="200 OK"
+        MESSAGE="Repository HEAD updated to ${NEW_HEAD}."
+    else
+        MESSAGE="Repository HEAD could not be updated to ${NEW_HEAD}."
+    fi
+}
+
+echo "Content-type: text/html"
+REPO=${REQUEST_URI##/a/projects/}
+REPO="${REPO//%2F//}"
+REPO="${REPO%%.git}"
+
+if test "${REQUEST_METHOD}" == "PUT"; then
+    if [[ ${REQUEST_URI} == */HEAD ]]; then
+        REPO=${REPO%"/HEAD"}
+        update_head
+    else
+        new
+    fi
+elif test "${REQUEST_METHOD}" == "DELETE"; then
+    delete
+else
+    STATUS_CODE="400 Bad Request"
+    MESSAGE="Unknown method."
+fi
+
+test -z ${STATUS_CODE} && STATUS_CODE="500 Internal Server Error"
+
+echo "Status: ${STATUS_CODE}"
+echo ""
+echo "${MESSAGE}"
diff --git a/charts/k8s-gerrit/container-images/apache-git-http-backend/tools/start b/charts/k8s-gerrit/container-images/apache-git-http-backend/tools/start
new file mode 100755
index 0000000..6b47114
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/apache-git-http-backend/tools/start
@@ -0,0 +1,4 @@
+#!/bin/ash
+
+/usr/sbin/apachectl start \
+  && tail -F -q -n +1 /var/log/apache2/*.log
diff --git a/charts/k8s-gerrit/container-images/base/Dockerfile b/charts/k8s-gerrit/container-images/base/Dockerfile
new file mode 100644
index 0000000..120a1a0
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/base/Dockerfile
@@ -0,0 +1,11 @@
+ARG BASE_IMAGE=alpine:3.18.2
+FROM $BASE_IMAGE
+
+ENV LC_ALL=C.UTF-8
+ENV LANG=C.UTF-8
+
+RUN apk update && \
+    apk add --no-cache git
+
+ARG GERRIT_UID=1000
+RUN adduser -D gerrit -u $GERRIT_UID -G users
diff --git a/charts/k8s-gerrit/container-images/base/README.md b/charts/k8s-gerrit/container-images/base/README.md
new file mode 100644
index 0000000..59533cb
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/base/README.md
@@ -0,0 +1,10 @@
+# Base image
+
+This is the base Docker image for Gerrit deployment on Kubernetes.
+It is only used in the build process and not published on Dockerhub.
+
+## Content
+
+* Alpine Linux 3.10.0
+* git
+* create `gerrit`-user as a non-root user to run the applications
diff --git a/charts/k8s-gerrit/container-images/gerrit-base/Dockerfile b/charts/k8s-gerrit/container-images/gerrit-base/Dockerfile
new file mode 100644
index 0000000..1f08ac6
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-base/Dockerfile
@@ -0,0 +1,57 @@
+ARG TAG=latest
+FROM base:${TAG}
+
+RUN apk update && \
+    apk add --no-cache \
+      coreutils \
+      curl \
+      openssh-keygen \
+      openjdk11
+
+RUN mkdir -p /var/gerrit/bin && \
+    mkdir -p /var/gerrit/etc && \
+    mkdir -p /var/gerrit/plugins && \
+    mkdir -p /var/plugins && \
+    mkdir -p /var/war
+
+# Download Gerrit release
+# TODO: Revert back to use release versions as soon as change 383334 has been released
+ARG GERRIT_WAR_URL=https://gerrit-ci.gerritforge.com/view/Gerrit/job/Gerrit-bazel-stable-3.8/lastSuccessfulBuild/artifact/gerrit/bazel-bin/release.war
+RUN curl -k -o /var/war/gerrit.war ${GERRIT_WAR_URL} && \
+    ln -s /var/war/gerrit.war /var/gerrit/bin/gerrit.war
+
+# Download healthcheck plugin
+ARG HEALTHCHECK_JAR_URL=https://gerrit-ci.gerritforge.com/view/Plugins-stable-3.8/job/plugin-healthcheck-bazel-stable-3.8/lastSuccessfulBuild/artifact/bazel-bin/plugins/healthcheck/healthcheck.jar
+RUN curl -k -o /var/plugins/healthcheck.jar ${HEALTHCHECK_JAR_URL} && \
+    ln -s /var/plugins/healthcheck.jar /var/gerrit/plugins/healthcheck.jar
+
+# Download global-refdb lib
+ARG GLOBAL_REFDB_URL=https://gerrit-ci.gerritforge.com/view/Plugins-stable-3.8/job/module-global-refdb-bazel-stable-3.8/lastSuccessfulBuild/artifact/bazel-bin/plugins/global-refdb/global-refdb.jar
+RUN curl -k -o /var/plugins/global-refdb.jar ${GLOBAL_REFDB_URL}
+
+# Download high-availability plugin
+ARG HA_JAR_URL=https://gerrit-ci.gerritforge.com/view/Plugins-stable-3.8/job/plugin-high-availability-bazel-stable-3.8/lastSuccessfulBuild/artifact/bazel-bin/plugins/high-availability/high-availability.jar
+RUN curl -k -o /var/plugins/high-availability.jar ${HA_JAR_URL}
+
+# Download zookeeper-refdb plugin
+ARG ZOOKEEPER_REFDB_URL=https://gerrit-ci.gerritforge.com/view/Plugins-stable-3.8/job/plugin-zookeeper-refdb-bazel-stable-3.8/lastSuccessfulBuild/artifact/bazel-bin/plugins/zookeeper-refdb/zookeeper-refdb.jar
+RUN curl -k -o /var/plugins/zookeeper-refdb.jar ${ZOOKEEPER_REFDB_URL}
+
+# Download spanner-refdb plugin
+ARG SPANNER_REFDB_URL=https://gerrit-ci.gerritforge.com/view/Plugins-stable-3.8/job/plugin-spanner-refdb-bazel-master-stable-3.8/lastSuccessfulBuild/artifact/bazel-bin/plugins/spanner-refdb/spanner-refdb.jar
+RUN curl -k -o /var/plugins/spanner-refdb.jar ${SPANNER_REFDB_URL}
+
+# Allow incoming traffic
+EXPOSE 29418 8080
+
+RUN chown -R gerrit:users /var/gerrit && \
+    chown -R gerrit:users /var/plugins && \
+    chown -R gerrit:users /var/war
+USER gerrit
+
+RUN java -jar /var/gerrit/bin/gerrit.war init \
+      --batch \
+      --no-auto-start \
+      -d /var/gerrit
+
+ENTRYPOINT ["ash", "/var/tools/start"]
diff --git a/charts/k8s-gerrit/container-images/gerrit-base/README.md b/charts/k8s-gerrit/container-images/gerrit-base/README.md
new file mode 100644
index 0000000..4d67048
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-base/README.md
@@ -0,0 +1,25 @@
+# Gerrit base image
+
+Gerrit base image for Gerrit and Gerrit replica images.
+It is only used in the build process and not published on Dockerhub.
+
+## Content
+
+* base image
+* curl
+* openssh-keygen
+* OpenJDK 11
+* gerrit.war
+
+## Setup and configuration
+
+* install package dependencies
+* create base folders for gerrit binary and gerrit configuration
+* download gerrit.war from provided URL
+* prepare filesystem permissions for gerrit user
+* open ports for incoming traffic
+* initialize default Gerrit site
+
+## Start
+
+* starts the container via start script `/var/tools/start`
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/.dockerignore b/charts/k8s-gerrit/container-images/gerrit-init/.dockerignore
new file mode 100644
index 0000000..7c68535
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/.dockerignore
@@ -0,0 +1 @@
+tools/__pycache__
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/Dockerfile b/charts/k8s-gerrit/container-images/gerrit-init/Dockerfile
new file mode 100644
index 0000000..70da7aa
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/Dockerfile
@@ -0,0 +1,29 @@
+ARG TAG=latest
+FROM gerrit-base:${TAG}
+
+USER root
+
+COPY dependencies/* /var/tools/
+COPY requirements.txt /var/tools/
+WORKDIR /var/tools
+
+RUN apk update && \
+    apk add --no-cache \
+      python3 && \
+    python3 -m ensurepip && \
+    rm -r /usr/lib/python*/ensurepip && \
+    # follow https://til.simonwillison.net/python/pip-tools to update hashes
+    pip3 install --require-hashes -r requirements.txt --no-cache --upgrade && \
+    pipenv install --python 3.11 --system
+
+COPY tools /var/tools/
+COPY config/* /var/config/
+
+RUN mkdir -p /var/mnt/git \
+  && mkdir -p /var/mnt/logs \
+  && chown -R gerrit:users /var/mnt
+
+USER gerrit
+
+ENTRYPOINT ["python3", "/var/tools/gerrit-initializer"]
+CMD ["-s", "/var/gerrit", "-c", "/var/config/gerrit-init.yaml", "init"]
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/README.md b/charts/k8s-gerrit/container-images/gerrit-init/README.md
new file mode 100644
index 0000000..37b5bda
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/README.md
@@ -0,0 +1,64 @@
+# Gerrit replica init container image
+
+Kubernetes init container for initializing gerrit. The python script running in
+the container initializes Gerrit including the installation of configured
+plugins.
+
+## Content
+
+* gerrit-base image
+
+## Setup and configuration
+
+* install python 3
+* copy tool scripts
+
+## Start
+
+* start the container via start script `python3 /var/tools/gerrit-initializer init`
+
+The `main.py init`-command
+
+* reads configuration from gerrit.config (via `gerrit_config_parser.py`)
+* initializes Gerrit
+
+The `main.py validate_notedb`-command
+
+* validates and waits for the repository `All-Projects.git` with the refs
+`refs/meta/config`.
+* validates and waits for the repository `All-Users.git` with the ref
+`refs/meta/config`.
+
+## Configuration
+
+The configuration format looks as follows:
+
+```yaml
+plugins: []
+# A plugin packaged in the gerrit.war-file
+# - name: download-commands
+
+# A plugin packaged in the gerrit.war-file that will also be installed as a
+# lib
+# - name: replication
+#   installAsLibrary: true
+
+# A plugin that will be downloaded on startup
+# - name: delete-project
+#   url: https://example.com/gerrit-plugins/delete-project.jar
+#   sha1:
+#   installAsLibrary: false
+libs: []
+# A lib that will be downloaded on startup
+# - name: global-refdb
+#   url: https://example.com/gerrit-plugins/global-refdb.jar
+#   sha1:
+#DEPRECATED: `pluginCache` was deprecated in favor of `pluginCacheEnabled`
+# pluginCache: true
+pluginCacheEnabled: false
+pluginCacheDir: null
+# Can be either true to use default CA certificates, false to disable SSL
+# verification or a path to a custom CA certificate store.
+caCertPath: true
+highAvailability: false
+```
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/config/gerrit-init.yaml b/charts/k8s-gerrit/container-images/gerrit-init/config/gerrit-init.yaml
new file mode 100644
index 0000000..65b7b28
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/config/gerrit-init.yaml
@@ -0,0 +1 @@
+pluginCache: false
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/dependencies/Pipfile b/charts/k8s-gerrit/container-images/gerrit-init/dependencies/Pipfile
new file mode 100644
index 0000000..6a55c64
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/dependencies/Pipfile
@@ -0,0 +1,13 @@
+[[source]]
+name = "pypi"
+url = "https://pypi.org/simple"
+verify_ssl = true
+
+[dev-packages]
+
+[packages]
+pyyaml = "~=6.0"
+requests = "~=2.31.0"
+
+[requires]
+python_version = "3.11"
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/dependencies/Pipfile.lock b/charts/k8s-gerrit/container-images/gerrit-init/dependencies/Pipfile.lock
new file mode 100644
index 0000000..10e814e
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/dependencies/Pipfile.lock
@@ -0,0 +1,180 @@
+{
+    "_meta": {
+        "hash": {
+            "sha256": "bf7e62c1c2c8f726ef7dab0c66bddf079c2f4cee97a1d5a4d4546fcc4f41600f"
+        },
+        "pipfile-spec": 6,
+        "requires": {
+            "python_version": "3.11"
+        },
+        "sources": [
+            {
+                "name": "pypi",
+                "url": "https://pypi.org/simple",
+                "verify_ssl": true
+            }
+        ]
+    },
+    "default": {
+        "certifi": {
+            "hashes": [
+                "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7",
+                "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==2023.5.7"
+        },
+        "charset-normalizer": {
+            "hashes": [
+                "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96",
+                "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c",
+                "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710",
+                "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706",
+                "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020",
+                "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252",
+                "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad",
+                "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329",
+                "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a",
+                "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f",
+                "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6",
+                "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4",
+                "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a",
+                "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46",
+                "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2",
+                "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23",
+                "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace",
+                "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd",
+                "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982",
+                "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10",
+                "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2",
+                "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea",
+                "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09",
+                "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5",
+                "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149",
+                "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489",
+                "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9",
+                "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80",
+                "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592",
+                "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3",
+                "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6",
+                "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed",
+                "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c",
+                "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200",
+                "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a",
+                "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e",
+                "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d",
+                "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6",
+                "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623",
+                "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669",
+                "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3",
+                "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa",
+                "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9",
+                "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2",
+                "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f",
+                "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1",
+                "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4",
+                "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a",
+                "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8",
+                "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3",
+                "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029",
+                "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f",
+                "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959",
+                "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22",
+                "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7",
+                "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952",
+                "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346",
+                "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e",
+                "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d",
+                "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299",
+                "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd",
+                "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a",
+                "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3",
+                "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037",
+                "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94",
+                "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c",
+                "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858",
+                "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a",
+                "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449",
+                "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c",
+                "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918",
+                "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1",
+                "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c",
+                "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac",
+                "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"
+            ],
+            "markers": "python_full_version >= '3.7.0'",
+            "version": "==3.2.0"
+        },
+        "idna": {
+            "hashes": [
+                "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
+                "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"
+            ],
+            "markers": "python_version >= '3.5'",
+            "version": "==3.4"
+        },
+        "pyyaml": {
+            "hashes": [
+                "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc",
+                "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741",
+                "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206",
+                "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27",
+                "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595",
+                "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62",
+                "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98",
+                "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696",
+                "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d",
+                "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867",
+                "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47",
+                "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486",
+                "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6",
+                "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3",
+                "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007",
+                "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938",
+                "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c",
+                "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735",
+                "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d",
+                "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba",
+                "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8",
+                "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5",
+                "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd",
+                "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3",
+                "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0",
+                "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515",
+                "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c",
+                "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c",
+                "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924",
+                "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34",
+                "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43",
+                "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859",
+                "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673",
+                "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a",
+                "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab",
+                "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa",
+                "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c",
+                "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585",
+                "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d",
+                "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"
+            ],
+            "index": "pypi",
+            "version": "==6.0.1"
+        },
+        "requests": {
+            "hashes": [
+                "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f",
+                "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"
+            ],
+            "index": "pypi",
+            "version": "==2.31.0"
+        },
+        "urllib3": {
+            "hashes": [
+                "sha256:48e7fafa40319d358848e1bc6809b208340fafe2096f1725d05d67443d0483d1",
+                "sha256:bee28b5e56addb8226c96f7f13ac28cb4c301dd5ea8a6ca179c0b9835e032825"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==2.0.3"
+        }
+    },
+    "develop": {}
+}
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/requirements.in b/charts/k8s-gerrit/container-images/gerrit-init/requirements.in
new file mode 100644
index 0000000..a7f62a1
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/requirements.in
@@ -0,0 +1,3 @@
+setuptools
+wheel
+pipenv
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/requirements.txt b/charts/k8s-gerrit/container-images/gerrit-init/requirements.txt
new file mode 100644
index 0000000..c2de41b
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/requirements.txt
@@ -0,0 +1,46 @@
+#
+# This file is autogenerated by pip-compile with Python 3.10
+# by the following command:
+#
+#    pip-compile --allow-unsafe --generate-hashes requirements.in
+#
+certifi==2022.12.7 \
+    --hash=sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3 \
+    --hash=sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18
+    # via pipenv
+distlib==0.3.6 \
+    --hash=sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46 \
+    --hash=sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e
+    # via virtualenv
+filelock==3.9.0 \
+    --hash=sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de \
+    --hash=sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d
+    # via virtualenv
+pipenv==2023.2.18 \
+    --hash=sha256:4e45226d197ad84fa11a9d944cb0e1bfcc197919944d0af96e55adf7e1fdc76c \
+    --hash=sha256:ecbe4e301616c5fa3128d557507d79a35d895cd922139969929d7357b66b1509
+    # via -r requirements.in
+platformdirs==3.0.0 \
+    --hash=sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9 \
+    --hash=sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567
+    # via virtualenv
+virtualenv==20.19.0 \
+    --hash=sha256:37a640ba82ed40b226599c522d411e4be5edb339a0c0de030c0dc7b646d61590 \
+    --hash=sha256:54eb59e7352b573aa04d53f80fc9736ed0ad5143af445a1e539aada6eb947dd1
+    # via pipenv
+virtualenv-clone==0.5.7 \
+    --hash=sha256:418ee935c36152f8f153c79824bb93eaf6f0f7984bae31d3f48f350b9183501a \
+    --hash=sha256:44d5263bceed0bac3e1424d64f798095233b64def1c5689afa43dc3223caf5b0
+    # via pipenv
+wheel==0.38.4 \
+    --hash=sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac \
+    --hash=sha256:b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8
+    # via -r requirements.in
+
+# The following packages are considered to be unsafe in a requirements file:
+setuptools==67.4.0 \
+    --hash=sha256:e5fd0a713141a4a105412233c63dc4e17ba0090c8e8334594ac790ec97792330 \
+    --hash=sha256:f106dee1b506dee5102cc3f3e9e68137bbad6d47b616be7991714b0c62204251
+    # via
+    #   -r requirements.in
+    #   pipenv
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/__main__.py b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/__main__.py
new file mode 100644
index 0000000..e49cc31
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/__main__.py
@@ -0,0 +1,18 @@
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from main import main
+
+if __name__ == "__main__":
+    main()
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/__init__.py b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/__init__.py
new file mode 100644
index 0000000..2230656
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/__init__.py
@@ -0,0 +1,13 @@
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/config/__init__.py b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/config/__init__.py
new file mode 100644
index 0000000..2230656
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/config/__init__.py
@@ -0,0 +1,13 @@
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/config/init_config.py b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/config/init_config.py
new file mode 100644
index 0000000..68a6f00
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/config/init_config.py
@@ -0,0 +1,90 @@
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os.path
+
+import yaml
+
+
+class InitConfig:
+    def __init__(self):
+        self.plugins = []
+        self.libs = []
+        self.plugin_cache_enabled = False
+        self.plugin_cache_dir = None
+
+        self.ca_cert_path = True
+
+        self.is_ha = False
+        self.refdb = False
+
+    def parse(self, config_file):
+        if not os.path.exists(config_file):
+            raise FileNotFoundError(f"Could not find config file: {config_file}")
+
+        with open(config_file, "r", encoding="utf-8") as f:
+            config = yaml.load(f, Loader=yaml.SafeLoader)
+
+        if config is None:
+            raise ValueError(f"Invalid config-file: {config_file}")
+
+        if "plugins" in config:
+            self.plugins = config["plugins"]
+        if "libs" in config:
+            self.libs = config["libs"]
+        # DEPRECATED: `pluginCache` was deprecated in favor of `pluginCacheEnabled`
+        if "pluginCache" in config:
+            self.plugin_cache_enabled = config["pluginCache"]
+        if "pluginCacheEnabled" in config:
+            self.plugin_cache_enabled = config["pluginCacheEnabled"]
+        if "pluginCacheDir" in config and config["pluginCacheDir"]:
+            self.plugin_cache_dir = config["pluginCacheDir"]
+
+        if "caCertPath" in config:
+            self.ca_cert_path = config["caCertPath"]
+
+        self.is_ha = "highAvailability" in config and config["highAvailability"]
+        if "refdb" in config:
+            self.refdb = config["refdb"]
+
+        return self
+
+    def get_plugins(self):
+        return self.plugins
+
+    def get_plugin_names(self):
+        return set([p["name"] for p in self.plugins])
+
+    def get_libs(self):
+        return self.libs
+
+    def get_lib_names(self):
+        return set([p["name"] for p in self.libs])
+
+    def get_packaged_plugins(self):
+        return list(filter(lambda x: "url" not in x, self.plugins))
+
+    def get_downloaded_plugins(self):
+        return list(filter(lambda x: "url" in x, self.plugins))
+
+    def get_plugins_installed_as_lib(self):
+        return [
+            lib["name"]
+            for lib in list(
+                filter(
+                    lambda x: "installAsLibrary" in x and x["installAsLibrary"],
+                    self.plugins,
+                )
+            )
+        ]
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/helpers/__init__.py b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/helpers/__init__.py
new file mode 100644
index 0000000..2230656
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/helpers/__init__.py
@@ -0,0 +1,13 @@
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/helpers/git.py b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/helpers/git.py
new file mode 100644
index 0000000..f21b28d
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/helpers/git.py
@@ -0,0 +1,76 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import subprocess
+
+
+class GitConfigParser:
+    def __init__(self, config_path):
+        self.path = config_path
+
+    def _execute_shell_command_and_get_output_lines(self, command):
+        sub_process_run = subprocess.run(
+            command.split(), stdout=subprocess.PIPE, check=True, universal_newlines=True
+        )
+        return [line.strip() for line in sub_process_run.stdout.splitlines()]
+
+    def _get_value(self, key):
+        command = f"git config -f {self.path} --get {key}"
+        return self._execute_shell_command_and_get_output_lines(command)
+
+    def list(self):
+        command = f"git config -f {self.path} --list"
+        options = self._execute_shell_command_and_get_output_lines(command)
+        option_list = []
+        for opt in options:
+            parsed_opt = {}
+            full_key, value = opt.split("=", 1)
+            parsed_opt["value"] = value
+            full_key = full_key.split(".")
+            parsed_opt["section"] = full_key[0]
+            if len(full_key) == 2:
+                parsed_opt["subsection"] = None
+                parsed_opt["key"] = full_key[1]
+            elif len(full_key) == 3:
+                parsed_opt["subsection"] = full_key[1]
+                parsed_opt["key"] = full_key[2]
+            option_list.append(parsed_opt)
+
+        return option_list
+
+    def get(self, key, default=None):
+        """
+        Returns value of given key in the configuration file. If the key appears
+        multiple times, the last value is returned.
+        """
+        try:
+            return self._get_value(key)[-1]
+        except subprocess.CalledProcessError:
+            return default
+
+    def get_boolean(self, key, default=False):
+        """
+        Returns boolean value of given key in the configuration file. If the key
+        appears multiple times, the last value is returned.
+        """
+        if not isinstance(default, bool):
+            raise TypeError("Default has to be a boolean.")
+
+        try:
+            value = self._get_value(key)[-1].lower()
+            if value not in ["true", "false"]:
+                raise TypeError("Value is not a boolean.")
+            return value == "true"
+        except subprocess.CalledProcessError:
+            return default
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/helpers/log.py b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/helpers/log.py
new file mode 100644
index 0000000..06aa72c
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/helpers/log.py
@@ -0,0 +1,26 @@
+#!/usr/bin/python3
+
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import logging
+
+
+def get_logger(name):
+    log = logging.Logger(name)
+    handler = logging.StreamHandler()
+    handler.setFormatter(logging.Formatter("[%(asctime)s] %(levelname)s %(message)s"))
+    log.addHandler(handler)
+    log.setLevel(logging.DEBUG)
+    return log
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/__init__.py b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/__init__.py
new file mode 100644
index 0000000..2230656
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/__init__.py
@@ -0,0 +1,13 @@
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/download_plugins.py b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/download_plugins.py
new file mode 100755
index 0000000..2c9ace0
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/download_plugins.py
@@ -0,0 +1,372 @@
+#!/usr/bin/python3
+
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import hashlib
+import os
+import shutil
+import time
+
+from abc import ABC, abstractmethod
+from zipfile import ZipFile
+
+import requests
+
+from ..helpers import log
+
+LOG = log.get_logger("init")
+MAX_LOCK_LIFETIME = 60
+MAX_CACHED_VERSIONS = 5
+
+REQUIRED_PLUGINS = ["healthcheck"]
+REQUIRED_HA_PLUGINS = ["high-availability"]
+REQUIRED_HA_LIBS = ["high-availability", "global-refdb"]
+
+
+class InvalidPluginException(Exception):
+    """Exception to be raised, if the downloaded plugin is not valid."""
+
+
+class MissingRequiredPluginException(Exception):
+    """Exception to be raised, if the downloaded plugin is not valid."""
+
+
+class AbstractPluginInstaller(ABC):
+    def __init__(self, site, config):
+        self.site = site
+        self.config = config
+
+        self.required_plugins = self._get_required_plugins()
+        self.required_libs = self._get_required_libs()
+
+        self.plugin_dir = os.path.join(site, "plugins")
+        self.lib_dir = os.path.join(site, "lib")
+        self.plugins_changed = False
+
+    def _create_plugins_dir(self):
+        if not os.path.exists(self.plugin_dir):
+            os.makedirs(self.plugin_dir)
+            LOG.info("Created plugin installation directory: %s", self.plugin_dir)
+
+    def _create_lib_dir(self):
+        if not os.path.exists(self.lib_dir):
+            os.makedirs(self.lib_dir)
+            LOG.info("Created lib installation directory: %s", self.lib_dir)
+
+    def _get_installed_plugins(self):
+        return self._get_installed_jars(self.plugin_dir)
+
+    def _get_installed_libs(self):
+        return self._get_installed_jars(self.lib_dir)
+
+    @staticmethod
+    def _get_installed_jars(dir):
+        if os.path.exists(dir):
+            return [f for f in os.listdir(dir) if f.endswith(".jar")]
+
+        return []
+
+    def _get_required_plugins(self):
+        required = REQUIRED_PLUGINS.copy()
+        if self.config.is_ha:
+            required.extend(REQUIRED_HA_PLUGINS)
+        if self.config.refdb:
+            required.append(f"{self.config.refdb}-refdb")
+        LOG.info("Requiring plugins: %s", required)
+        return required
+
+    def _get_required_libs(self):
+        required = []
+        if self.config.is_ha:
+            required.extend(REQUIRED_HA_LIBS)
+        LOG.info("Requiring libs: %s", required)
+        return required
+
+    def _install_required_plugins(self):
+        for plugin in self.required_plugins:
+            if plugin in self.config.get_plugin_names():
+                continue
+
+            self._install_required_jar(plugin, self.plugin_dir)
+
+    def _install_required_libs(self):
+        for lib in self.required_libs:
+            if lib in self.config.get_lib_names():
+                continue
+
+            self._install_required_jar(lib, self.lib_dir)
+
+    def _install_required_jar(self, jar, target_dir):
+        with ZipFile("/var/war/gerrit.war", "r") as war:
+            # Lib modules can be packaged as a plugin. However, they could
+            # currently not be installed by the init pgm tool.
+            if f"WEB-INF/plugins/{jar}.jar" in war.namelist():
+                self._install_plugin_from_war(jar, target_dir)
+                return
+        try:
+            self._install_jar_from_container(jar, target_dir)
+        except FileNotFoundError:
+            raise MissingRequiredPluginException(f"Required jar {jar} was not found.")
+
+    def _install_jar_from_container(self, plugin, target_dir):
+        source_file = os.path.join("/var/plugins", plugin + ".jar")
+        target_file = os.path.join(target_dir, plugin + ".jar")
+        LOG.info(
+            "Installing plugin %s from container to %s.",
+            plugin,
+            target_file,
+        )
+        if not os.path.exists(source_file):
+            raise FileNotFoundError(
+                "Unable to find required plugin in container: " + plugin
+            )
+        if os.path.exists(target_file) and self._get_file_sha(
+            source_file
+        ) == self._get_file_sha(target_file):
+            return
+
+        shutil.copyfile(source_file, target_file)
+        self.plugins_changed = True
+
+    def _install_plugins_from_war(self):
+        for plugin in self.config.get_packaged_plugins():
+            self._install_plugin_from_war(plugin["name"], self.plugin_dir)
+
+    def _install_plugin_from_war(self, plugin, target_dir):
+        LOG.info("Installing packaged plugin %s.", plugin)
+        with ZipFile("/var/war/gerrit.war", "r") as war:
+            war.extract(f"WEB-INF/plugins/{plugin}.jar", self.plugin_dir)
+
+        source_file = f"{self.plugin_dir}/WEB-INF/plugins/{plugin}.jar"
+        target_file = os.path.join(target_dir, f"{plugin}.jar")
+        if not os.path.exists(target_file) or self._get_file_sha(
+            source_file
+        ) != self._get_file_sha(target_file):
+            os.rename(source_file, target_file)
+            self.plugins_changed = True
+
+        shutil.rmtree(os.path.join(self.plugin_dir, "WEB-INF"), ignore_errors=True)
+
+    @staticmethod
+    def _get_file_sha(file):
+        file_hash = hashlib.sha1()
+        with open(file, "rb") as f:
+            while True:
+                chunk = f.read(64000)
+                if not chunk:
+                    break
+                file_hash.update(chunk)
+
+        LOG.debug("SHA1 of file '%s' is %s", file, file_hash.hexdigest())
+
+        return file_hash.hexdigest()
+
+    def _remove_unwanted_plugins(self):
+        wanted_plugins = list(self.config.get_plugins())
+        wanted_plugins.extend(self.required_plugins)
+        self._remove_unwanted(
+            wanted_plugins, self._get_installed_plugins(), self.plugin_dir
+        )
+
+    def _remove_unwanted_libs(self):
+        wanted_libs = list(self.config.get_libs())
+        wanted_libs.extend(self.required_libs)
+        wanted_libs.extend(self.config.get_plugins_installed_as_lib())
+        self._remove_unwanted(wanted_libs, self._get_installed_libs(), self.lib_dir)
+
+    @staticmethod
+    def _remove_unwanted(wanted, installed, dir):
+        for plugin in installed:
+            if os.path.splitext(plugin)[0] not in wanted:
+                os.remove(os.path.join(dir, plugin))
+                LOG.info("Removed plugin %s", plugin)
+
+    def _symlink_plugins_to_lib(self):
+        if not os.path.exists(self.lib_dir):
+            os.makedirs(self.lib_dir)
+        else:
+            for f in os.listdir(self.lib_dir):
+                path = os.path.join(self.lib_dir, f)
+                if (
+                    os.path.islink(path)
+                    and os.path.splitext(f)[0]
+                    not in self.config.get_plugins_installed_as_lib()
+                ):
+                    os.unlink(path)
+                    LOG.info("Removed symlink %s", f)
+        for lib in self.config.get_plugins_installed_as_lib():
+            plugin_path = os.path.join(self.plugin_dir, f"{lib}.jar")
+            if os.path.exists(plugin_path):
+                try:
+                    os.symlink(plugin_path, os.path.join(self.lib_dir, f"{lib}.jar"))
+                except FileExistsError:
+                    continue
+            else:
+                raise FileNotFoundError(
+                    f"Could not find plugin {lib} to symlink to lib-directory."
+                )
+
+    def execute(self):
+        self._create_plugins_dir()
+        self._create_lib_dir()
+
+        self._remove_unwanted_plugins()
+        self._remove_unwanted_libs()
+
+        self._install_required_plugins()
+        self._install_required_libs()
+
+        self._install_plugins_from_war()
+
+        for plugin in self.config.get_downloaded_plugins():
+            self._install_plugin(plugin)
+
+        for plugin in self.config.get_libs():
+            self._install_lib(plugin)
+
+        self._symlink_plugins_to_lib()
+
+    def _download_plugin(self, plugin, target):
+        LOG.info("Downloading %s plugin to %s", plugin["name"], target)
+        try:
+            response = requests.get(plugin["url"])
+        except requests.exceptions.SSLError:
+            response = requests.get(plugin["url"], verify=self.config.ca_cert_path)
+
+        with open(target, "wb") as f:
+            f.write(response.content)
+
+        file_sha = self._get_file_sha(target)
+
+        if file_sha != plugin["sha1"]:
+            os.remove(target)
+            raise InvalidPluginException(
+                (
+                    f"SHA1 of downloaded file ({file_sha}) did not match "
+                    f"expected SHA1 ({plugin['sha1']}). "
+                    f"Removed downloaded file ({target})"
+                )
+            )
+
+    def _install_plugin(self, plugin):
+        self._install_jar(plugin, self.plugin_dir)
+
+    def _install_lib(self, lib):
+        self._install_jar(lib, self.lib_dir)
+
+    @abstractmethod
+    def _install_jar(self, plugin, target_dir):
+        pass
+
+
+class PluginInstaller(AbstractPluginInstaller):
+    def _install_jar(self, plugin, target_dir):
+        target = os.path.join(target_dir, f"{plugin['name']}.jar")
+        if os.path.exists(target) and self._get_file_sha(target) == plugin["sha1"]:
+            return
+
+        self._download_plugin(plugin, target)
+
+        self.plugins_changed = True
+
+
+class CachedPluginInstaller(AbstractPluginInstaller):
+    @staticmethod
+    def _cleanup_cache(plugin_cache_dir):
+        cached_files = [
+            os.path.join(plugin_cache_dir, f) for f in os.listdir(plugin_cache_dir)
+        ]
+        while len(cached_files) > MAX_CACHED_VERSIONS:
+            oldest_file = min(cached_files, key=os.path.getctime)
+            LOG.info(
+                "Too many cached files in %s. Removing file %s",
+                plugin_cache_dir,
+                oldest_file,
+            )
+            os.remove(oldest_file)
+            cached_files.remove(oldest_file)
+
+    @staticmethod
+    def _create_download_lock(lock_path):
+        with open(lock_path, "w", encoding="utf-8") as f:
+            f.write(os.environ["HOSTNAME"])
+            LOG.debug("Created download lock %s", lock_path)
+
+    @staticmethod
+    def _create_plugin_cache_dir(plugin_cache_dir):
+        if not os.path.exists(plugin_cache_dir):
+            os.makedirs(plugin_cache_dir)
+            LOG.info("Created cache directory %s", plugin_cache_dir)
+
+    def _get_cached_plugin_path(self, plugin):
+        return os.path.join(
+            self.config.plugin_cache_dir,
+            plugin["name"],
+            f"{plugin['name']}-{plugin['sha1']}.jar",
+        )
+
+    def _install_from_cache_or_download(self, plugin, target):
+        cached_plugin_path = self._get_cached_plugin_path(plugin)
+
+        if os.path.exists(cached_plugin_path):
+            LOG.info("Installing %s plugin from cache.", plugin["name"])
+        else:
+            LOG.info("%s not found in cache. Downloading it.", plugin["name"])
+            self._create_plugin_cache_dir(os.path.dirname(cached_plugin_path))
+
+            lock_path = f"{cached_plugin_path}.lock"
+            while os.path.exists(lock_path):
+                LOG.info(
+                    "Download lock found (%s). Waiting %d seconds for it to be released.",
+                    lock_path,
+                    MAX_LOCK_LIFETIME,
+                )
+                lock_timestamp = os.path.getmtime(lock_path)
+                if time.time() > lock_timestamp + MAX_LOCK_LIFETIME:
+                    LOG.info("Stale download lock found (%s).", lock_path)
+                    self._remove_download_lock(lock_path)
+
+            self._create_download_lock(lock_path)
+
+            try:
+                self._download_plugin(plugin, cached_plugin_path)
+            finally:
+                self._remove_download_lock(lock_path)
+
+        shutil.copy(cached_plugin_path, target)
+        self._cleanup_cache(os.path.dirname(cached_plugin_path))
+
+    def _install_jar(self, plugin, target_dir):
+        install_path = os.path.join(target_dir, f"{plugin['name']}.jar")
+        if (
+            os.path.exists(install_path)
+            and self._get_file_sha(install_path) == plugin["sha1"]
+        ):
+            return
+
+        self.plugins_changed = True
+        self._install_from_cache_or_download(plugin, install_path)
+
+    @staticmethod
+    def _remove_download_lock(lock_path):
+        os.remove(lock_path)
+        LOG.debug("Removed download lock %s", lock_path)
+
+
+def get_installer(site, config):
+    plugin_installer = (
+        CachedPluginInstaller if config.plugin_cache_enabled else PluginInstaller
+    )
+    return plugin_installer(site, config)
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/init.py b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/init.py
new file mode 100755
index 0000000..4931984
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/init.py
@@ -0,0 +1,227 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import shutil
+import subprocess
+import sys
+
+from ..helpers import git, log
+from .download_plugins import get_installer
+from .reindex import IndexType, get_reindexer
+from .validate_notedb import NoteDbValidator
+
+LOG = log.get_logger("init")
+MNT_PATH = "/var/mnt"
+
+
+class GerritInit:
+    def __init__(self, site, config):
+        self.site = site
+        self.config = config
+
+        self.plugin_installer = get_installer(self.site, self.config)
+
+        self.gerrit_config = git.GitConfigParser(
+            os.path.join(MNT_PATH, "etc/config/gerrit.config")
+        )
+        self.is_online_reindex = self.gerrit_config.get_boolean(
+            "index.onlineUpgrade", True
+        )
+        self.force_offline_reindex = False
+        self.installed_plugins = self._get_installed_plugins()
+
+        self.is_replica = self.gerrit_config.get_boolean("container.replica")
+        self.pid_file = f"{self.site}/logs/gerrit.pid"
+
+    def _get_gerrit_version(self, gerrit_war_path):
+        command = f"java -jar {gerrit_war_path} version"
+        version_process = subprocess.run(
+            command.split(), stdout=subprocess.PIPE, check=True
+        )
+        return version_process.stdout.decode().strip()
+
+    def _get_installed_plugins(self):
+        plugin_path = os.path.join(self.site, "plugins")
+        installed_plugins = set()
+
+        if os.path.exists(plugin_path):
+            for f in os.listdir(plugin_path):
+                if os.path.isfile(os.path.join(plugin_path, f)) and f.endswith(".jar"):
+                    installed_plugins.add(os.path.splitext(f)[0])
+
+        return installed_plugins
+
+    def _gerrit_war_updated(self):
+        installed_war_path = os.path.join(self.site, "bin", "gerrit.war")
+        installed_version = self._get_gerrit_version(installed_war_path)
+        provided_version = self._get_gerrit_version("/var/war/gerrit.war")
+        LOG.info(
+            "Installed Gerrit version: %s; Provided Gerrit version: %s). ",
+            installed_version,
+            provided_version,
+        )
+        installed_minor_version = installed_version.split(".")[0:2]
+        provided_minor_version = provided_version.split(".")[0:2]
+
+        if (
+            not self.is_online_reindex
+            and installed_minor_version != provided_minor_version
+        ):
+            self.force_offline_reindex = True
+        return installed_version != provided_version
+
+    def _needs_init(self):
+        installed_war_path = os.path.join(self.site, "bin", "gerrit.war")
+        if not os.path.exists(installed_war_path):
+            LOG.info("Gerrit is not yet installed. Initializing new site.")
+            return True
+
+        if self._gerrit_war_updated():
+            LOG.info("Reinitializing site to perform update.")
+            return True
+
+        if self.plugin_installer.plugins_changed:
+            LOG.info("Plugins were installed or updated. Initializing.")
+            return True
+
+        if self.config.get_plugin_names().difference(self.installed_plugins):
+            LOG.info("Reininitializing site to install additional plugins.")
+            return True
+
+        LOG.info("No initialization required.")
+        return False
+
+    def _ensure_symlink(self, src, target):
+        if not os.path.exists(src):
+            raise FileNotFoundError(f"Unable to find mounted dir: {src}")
+
+        if os.path.islink(target) and os.path.realpath(target) == src:
+            return
+
+        if os.path.exists(target):
+            if os.path.isdir(target) and not os.path.islink(target):
+                shutil.rmtree(target)
+            else:
+                os.remove(target)
+
+        os.symlink(src, target)
+
+    def _symlink_mounted_site_components(self):
+        self._ensure_symlink(f"{MNT_PATH}/git", f"{self.site}/git")
+        self._ensure_symlink(f"{MNT_PATH}/logs", f"{self.site}/logs")
+
+        mounted_shared_dir = f"{MNT_PATH}/shared"
+        if not self.is_replica and os.path.exists(mounted_shared_dir):
+            self._ensure_symlink(mounted_shared_dir, f"{self.site}/shared")
+
+        index_type = self.gerrit_config.get("index.type", default=IndexType.LUCENE.name)
+        if IndexType[index_type.upper()] is IndexType.ELASTICSEARCH:
+            self._ensure_symlink(f"{MNT_PATH}/index", f"{self.site}/index")
+
+        data_dir = f"{self.site}/data"
+        if os.path.exists(data_dir):
+            for file_or_dir in os.listdir(data_dir):
+                abs_path = os.path.join(data_dir, file_or_dir)
+                if os.path.islink(abs_path) and not os.path.exists(
+                    os.path.realpath(abs_path)
+                ):
+                    os.unlink(abs_path)
+        else:
+            os.makedirs(data_dir)
+
+        mounted_data_dir = f"{MNT_PATH}/data"
+        if os.path.exists(mounted_data_dir):
+            for file_or_dir in os.listdir(mounted_data_dir):
+                abs_path = os.path.join(data_dir, file_or_dir)
+                abs_mounted_path = os.path.join(mounted_data_dir, file_or_dir)
+                if os.path.isdir(abs_mounted_path):
+                    self._ensure_symlink(abs_mounted_path, abs_path)
+
+    def _symlink_configuration(self):
+        etc_dir = f"{self.site}/etc"
+        if not os.path.exists(etc_dir):
+            os.makedirs(etc_dir)
+
+        for config_type in ["config", "secret"]:
+            if os.path.exists(f"{MNT_PATH}/etc/{config_type}"):
+                for file_or_dir in os.listdir(f"{MNT_PATH}/etc/{config_type}"):
+                    if os.path.isfile(
+                        os.path.join(f"{MNT_PATH}/etc/{config_type}", file_or_dir)
+                    ):
+                        self._ensure_symlink(
+                            os.path.join(f"{MNT_PATH}/etc/{config_type}", file_or_dir),
+                            os.path.join(etc_dir, file_or_dir),
+                        )
+
+    def _remove_auto_generated_ssh_keys(self):
+        etc_dir = f"{self.site}/etc"
+        if not os.path.exists(etc_dir):
+            return
+
+        for file_or_dir in os.listdir(etc_dir):
+            full_path = os.path.join(etc_dir, file_or_dir)
+            if os.path.isfile(full_path) and file_or_dir.startswith("ssh_host_"):
+                os.remove(full_path)
+
+    def execute(self):
+        if not self.is_replica:
+            self._symlink_mounted_site_components()
+        elif not NoteDbValidator(MNT_PATH).check():
+            LOG.info("NoteDB not ready. Initializing repositories.")
+            self._symlink_mounted_site_components()
+        self._symlink_configuration()
+
+        if os.path.exists(self.pid_file):
+            os.remove(self.pid_file)
+
+        self.plugin_installer.execute()
+
+        if self._needs_init():
+            if self.gerrit_config:
+                LOG.info("Existing gerrit.config found.")
+                dev_option = (
+                    "--dev"
+                    if self.gerrit_config.get(
+                        "auth.type", "development_become_any_account"
+                    ).lower()
+                    == "development_become_any_account"
+                    else ""
+                )
+            else:
+                LOG.info("No gerrit.config found. Initializing default site.")
+                dev_option = "--dev"
+
+            flags = f"--no-auto-start --batch {dev_option}"
+
+            command = f"java -jar /var/war/gerrit.war init {flags} -d {self.site}"
+
+            init_process = subprocess.run(
+                command.split(), stdout=subprocess.PIPE, check=True
+            )
+
+            if init_process.returncode > 0:
+                LOG.error(
+                    "An error occurred, when initializing Gerrit. Exit code: %d",
+                    init_process.returncode,
+                )
+                sys.exit(1)
+
+            self._remove_auto_generated_ssh_keys()
+            self._symlink_configuration()
+
+            if self.is_replica:
+                self._symlink_mounted_site_components()
+
+        get_reindexer(self.site, self.config).start(self.force_offline_reindex)
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/reindex.py b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/reindex.py
new file mode 100755
index 0000000..e5ec6df
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/reindex.py
@@ -0,0 +1,208 @@
+#!/usr/bin/python3
+
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import abc
+import enum
+import os.path
+import subprocess
+import sys
+
+import requests
+
+from ..helpers import git, log
+
+LOG = log.get_logger("reindex")
+MNT_PATH = "/var/mnt"
+INDEXES_PRIMARY = set(["accounts", "changes", "groups", "projects"])
+INDEXES_REPLICA = set(["groups"])
+
+
+class IndexType(enum.Enum):
+    LUCENE = enum.auto()
+    ELASTICSEARCH = enum.auto()
+
+
+class GerritAbstractReindexer(abc.ABC):
+    def __init__(self, gerrit_site_path, config):
+        self.gerrit_site_path = gerrit_site_path
+        self.index_config_path = f"{self.gerrit_site_path}/index/gerrit_index.config"
+        self.init_config = config
+
+        self.gerrit_config = git.GitConfigParser(
+            os.path.join(MNT_PATH, "etc/config/gerrit.config")
+        )
+        self.is_online_reindex = self.gerrit_config.get_boolean(
+            "index.onlineUpgrade", True
+        )
+        self.is_replica = self.gerrit_config.get_boolean("container.replica", False)
+
+        self.configured_indices = self._parse_gerrit_index_config()
+
+    @abc.abstractmethod
+    def _get_indices(self):
+        pass
+
+    def _parse_gerrit_index_config(self):
+        indices = {}
+        if os.path.exists(self.index_config_path):
+            config = git.GitConfigParser(self.index_config_path)
+            options = config.list()
+            for opt in options:
+                name, version = opt["subsection"].rsplit("_", 1)
+                ready = opt["value"].lower() == "true"
+                if name in indices:
+                    indices[name] = {
+                        "read": version if ready else indices[name]["read"],
+                        "latest_write": max(version, indices[name]["latest_write"]),
+                    }
+                else:
+                    indices[name] = {
+                        "read": version if ready else None,
+                        "latest_write": version,
+                    }
+        return indices
+
+    def _get_not_ready_indices(self):
+        not_ready_indices = []
+        for index, index_attrs in self.configured_indices.items():
+            if not index_attrs["read"]:
+                LOG.info("Index %s not ready.", index)
+                not_ready_indices.append(index)
+        index_set = INDEXES_REPLICA if self.is_replica else INDEXES_PRIMARY
+        not_ready_indices.extend(index_set.difference(self.configured_indices.keys()))
+        return not_ready_indices
+
+    def _indexes_need_update(self):
+        indices = self._get_indices()
+
+        if not indices:
+            return True
+
+        for index, index_attrs in self.configured_indices.items():
+            if (
+                index not in indices
+                or index_attrs["latest_write"] != indices[index]
+                or index_attrs["read"] != index_attrs["latest_write"]
+            ):
+                return True
+        return False
+
+    def reindex(self, indices=None):
+        LOG.info("Starting to reindex.")
+        command = f"java -jar /var/war/gerrit.war reindex -d {self.gerrit_site_path}"
+
+        if indices:
+            command += " ".join([f" --index {i}" for i in indices])
+
+        reindex_process = subprocess.run(
+            command.split(), stdout=subprocess.PIPE, check=True
+        )
+
+        if reindex_process.returncode > 0:
+            LOG.error(
+                "An error occurred, when reindexing Gerrit indices. Exit code: %d",
+                reindex_process.returncode,
+            )
+            sys.exit(1)
+
+        LOG.info("Finished reindexing.")
+
+    def start(self, is_forced):
+        if is_forced:
+            self.reindex()
+            return
+
+        if not self.configured_indices:
+            LOG.info("gerrit_index.config does not exist. Creating all indices.")
+            self.reindex()
+            return
+
+        not_ready_indices = self._get_not_ready_indices()
+        if not_ready_indices:
+            self.reindex(not_ready_indices)
+
+        if not self.is_online_reindex and self._indexes_need_update():
+            LOG.info("Not all indices are up-to-date.")
+            self.reindex()
+            return
+
+        LOG.info("Skipping reindexing.")
+
+
+class GerritLuceneReindexer(GerritAbstractReindexer):
+    def _get_indices(self):
+        file_list = os.listdir(os.path.join(self.gerrit_site_path, "index"))
+        file_list.remove("gerrit_index.config")
+        lucene_indices = {}
+        for index in file_list:
+            try:
+                (name, version) = index.split("_")
+                if name in lucene_indices:
+                    lucene_indices[name] = max(version, lucene_indices[name])
+                else:
+                    lucene_indices[name] = version
+            except ValueError:
+                LOG.debug("Ignoring invalid file in index-directory: %s", index)
+        return lucene_indices
+
+
+class GerritElasticSearchReindexer(GerritAbstractReindexer):
+    def _get_elasticsearch_config(self):
+        es_config = {}
+        gerrit_config = git.GitConfigParser(
+            os.path.join(self.gerrit_site_path, "etc", "gerrit.config")
+        )
+        es_config["prefix"] = gerrit_config.get(
+            "elasticsearch.prefix", default=""
+        ).lower()
+        es_config["server"] = gerrit_config.get(
+            "elasticsearch.server", default=""
+        ).lower()
+        return es_config
+
+    def _get_indices(self):
+        es_config = self._get_elasticsearch_config()
+        url = f"{es_config['server']}/{es_config['prefix']}*"
+        try:
+            response = requests.get(url)
+        except requests.exceptions.SSLError:
+            response = requests.get(url, verify=self.init_config.ca_cert_path)
+
+        es_indices = {}
+        for index, _ in response.json().items():
+            try:
+                index = index.replace(es_config["prefix"], "", 1)
+                (name, version) = index.split("_")
+                es_indices[name] = version
+            except ValueError:
+                LOG.debug("Found unknown index: %s", index)
+
+        return es_indices
+
+
+def get_reindexer(gerrit_site_path, config):
+    gerrit_config = git.GitConfigParser(
+        os.path.join(gerrit_site_path, "etc", "gerrit.config")
+    )
+    index_type = gerrit_config.get("index.type", default=IndexType.LUCENE.name)
+
+    if IndexType[index_type.upper()] is IndexType.LUCENE:
+        return GerritLuceneReindexer(gerrit_site_path, config)
+
+    if IndexType[index_type.upper()] is IndexType.ELASTICSEARCH:
+        return GerritElasticSearchReindexer(gerrit_site_path, config)
+
+    raise RuntimeError(f"Unknown index type {index_type}.")
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/validate_notedb.py b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/validate_notedb.py
new file mode 100644
index 0000000..aff9ce6
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/validate_notedb.py
@@ -0,0 +1,74 @@
+#!/usr/bin/python3
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import subprocess
+import time
+
+from ..helpers import log
+
+LOG = log.get_logger("init")
+
+
+class NoteDbValidator:
+    def __init__(self, site):
+        self.site = site
+
+        self.notedb_repos = ["All-Projects.git", "All-Users.git"]
+        self.required_refs = {
+            "All-Projects.git": ["refs/meta/config", "refs/meta/version"],
+            "All-Users.git": ["refs/meta/config"],
+        }
+
+    def _test_repo_exists(self, repo):
+        return os.path.exists(os.path.join(self.site, "git", repo))
+
+    def _test_ref_exists(self, repo, ref):
+        command = f"git --git-dir {self.site}/git/{repo} rev-parse --verify {ref}"
+        git_show_ref = subprocess.run(
+            command.split(),
+            stdout=subprocess.PIPE,
+            universal_newlines=True,
+            check=False,
+        )
+
+        return git_show_ref.returncode == 0
+
+    def wait_until_valid(self):
+        for repo in self.notedb_repos:
+            LOG.info("Waiting for repository %s.", repo)
+            while not self._test_repo_exists(repo):
+                time.sleep(1)
+            LOG.info("Found %s.", repo)
+
+            for ref in self.required_refs[repo]:
+                LOG.info("Waiting for ref %s in repository %s.", ref, repo)
+                while not self._test_ref_exists(repo, ref):
+                    time.sleep(1)
+                LOG.info("Found ref %s in repo %s.", ref, repo)
+
+    def check(self):
+        for repo in self.notedb_repos:
+            if not self._test_repo_exists(repo):
+                LOG.info("Repository %s is missing.", repo)
+                return False
+            LOG.info("Found %s.", repo)
+
+            for ref in self.required_refs[repo]:
+                if not self._test_ref_exists(repo, ref):
+                    LOG.info("Ref %s in repository %s is missing.", ref, repo)
+                    return False
+                LOG.info("Found ref %s in repo %s.", ref, repo)
+        return True
diff --git a/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/main.py b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/main.py
new file mode 100755
index 0000000..b41cf3a
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit-init/tools/gerrit-initializer/main.py
@@ -0,0 +1,93 @@
+#!/usr/bin/python3
+
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import argparse
+
+from initializer.tasks import download_plugins, init, reindex, validate_notedb
+from initializer.config.init_config import InitConfig
+
+
+def _run_download_plugins(args):
+    config = InitConfig().parse(args.config)
+    download_plugins.get_installer(args.site, config).execute()
+
+
+def _run_init(args):
+    config = InitConfig().parse(args.config)
+    init.GerritInit(args.site, config).execute()
+
+
+def _run_reindex(args):
+    config = InitConfig().parse(args.config)
+    reindex.get_reindexer(args.site, config).start(args.force)
+
+
+def _run_validate_notedb(args):
+    validate_notedb.NoteDbValidator(args.site).wait_until_valid()
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        "-s",
+        "--site",
+        help="Path to Gerrit site",
+        dest="site",
+        action="store",
+        default="/var/gerrit",
+        required=True,
+    )
+    parser.add_argument(
+        "-c",
+        "--config",
+        help="Path to configuration file for init process.",
+        dest="config",
+        action="store",
+        required=True,
+    )
+
+    subparsers = parser.add_subparsers()
+
+    parser_download_plugins = subparsers.add_parser(
+        "download-plugins", help="Download plugins"
+    )
+    parser_download_plugins.set_defaults(func=_run_download_plugins)
+
+    parser_init = subparsers.add_parser("init", help="Initialize Gerrit site")
+    parser_init.set_defaults(func=_run_init)
+
+    parser_reindex = subparsers.add_parser("reindex", help="Reindex Gerrit indexes")
+    parser_reindex.add_argument(
+        "-f",
+        "--force",
+        help="Reindex even if indices are ready.",
+        dest="force",
+        action="store_true",
+    )
+    parser_reindex.set_defaults(func=_run_reindex)
+
+    parser_validate_notedb = subparsers.add_parser(
+        "validate-notedb", help="Validate NoteDB"
+    )
+    parser_validate_notedb.set_defaults(func=_run_validate_notedb)
+
+    args = parser.parse_args()
+    args.func(args)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/charts/k8s-gerrit/container-images/gerrit/Dockerfile b/charts/k8s-gerrit/container-images/gerrit/Dockerfile
new file mode 100644
index 0000000..6a2eebc
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit/Dockerfile
@@ -0,0 +1,4 @@
+ARG TAG=latest
+FROM gerrit-base:${TAG}
+
+COPY tools/* /var/tools/
diff --git a/charts/k8s-gerrit/container-images/gerrit/README.md b/charts/k8s-gerrit/container-images/gerrit/README.md
new file mode 100644
index 0000000..2cd1b7a
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit/README.md
@@ -0,0 +1,13 @@
+# Gerrit image
+
+Container image for a Gerrit instance
+
+## Content
+
+* the [gerrit-base](../gerrit-base/README.md) image
+* `/var/tools/start`: start script
+
+## Start
+
+* starts Gerrit via start script `/var/tools/start` either as primary or replica
+  depending on the provided `gerrit.config`
diff --git a/charts/k8s-gerrit/container-images/gerrit/tools/start b/charts/k8s-gerrit/container-images/gerrit/tools/start
new file mode 100755
index 0000000..2073181
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/gerrit/tools/start
@@ -0,0 +1,13 @@
+#!/bin/ash
+GERRIT_DAEMON_OPTS="--console-log --enable-httpd"
+
+IS_REPLICA=$(git config -f /var/gerrit/etc/gerrit.config --get container.replica)
+if [[ "$IS_REPLICA" == "true" ]]; then
+  GERRIT_DAEMON_OPTS="$GERRIT_DAEMON_OPTS --replica"
+fi
+
+JAVA_OPTIONS=$(git config --file /var/gerrit/etc/gerrit.config --get-all container.javaOptions)
+JAVA_OPTIONS="$JAVA_OPTIONS -Dgerrit.instanceId=$POD_NAME"
+java ${JAVA_OPTIONS} -jar /var/gerrit/bin/gerrit.war daemon \
+  -d /var/gerrit \
+  $GERRIT_DAEMON_OPTS
diff --git a/charts/k8s-gerrit/container-images/git-gc/Dockerfile b/charts/k8s-gerrit/container-images/git-gc/Dockerfile
new file mode 100644
index 0000000..2293b12
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/git-gc/Dockerfile
@@ -0,0 +1,13 @@
+ARG TAG=latest
+FROM base:${TAG}
+
+COPY tools/* /var/tools/
+
+RUN mkdir -p /var/log/git && \
+    chown gerrit:users /var/log/git
+
+USER gerrit
+
+VOLUME ["/var/gerrit/git"]
+
+ENTRYPOINT ["/var/tools/gc.sh"]
diff --git a/charts/k8s-gerrit/container-images/git-gc/README.md b/charts/k8s-gerrit/container-images/git-gc/README.md
new file mode 100644
index 0000000..2509f99
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/git-gc/README.md
@@ -0,0 +1,19 @@
+# Git GC container image
+
+Container for running `git gc`. It is meant to run as a CronJob, when used in
+Kubernetes. It can also be used to run garbage collection on-demand, e.g. using
+a Kubernetes Job.
+
+## Content
+
+* base image
+* `gc.sh`: gc-script
+
+## Setup and configuration
+
+* copy tools scripts
+* ensure filesystem permissions
+
+## Start
+
+*  execution of the provided `gc.sh`
diff --git a/charts/k8s-gerrit/container-images/git-gc/tools/gc.sh b/charts/k8s-gerrit/container-images/git-gc/tools/gc.sh
new file mode 100755
index 0000000..e3ad4e0
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/git-gc/tools/gc.sh
@@ -0,0 +1,220 @@
+#!/bin/ash
+# Copyright (C) 2011, 2020 SAP SE
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+usage()
+{
+  echo "Usage: $0 [ -s ProjectName ] [ -p ProjectName ] [ -b ProjectName ]"
+  echo "-s ProjectName     : skip this project"
+  echo "-p ProjectName     : run git-gc for this project"
+  echo "-b ProjectName     : do not write bitmaps for this project"
+  echo ""
+  echo "By default the script will run git-gc for all projects unless \"-p\" option is provided"
+  echo
+  echo "Examples:"
+  echo "  Run git-gc for all projects but skip foo and bar/baz projects"
+  echo "    $0 -s foo -s bar/baz"
+  echo "  Run git-gc only for foo and bar/baz projects"
+  echo "    $0 -p foo -p bar/baz"
+  echo "  Run git-gc only for bar project without writing bitmaps"
+  echo "    $0 -p bar -b bar"
+  echo
+  echo "To specify a one-time --aggressive git gc for a repository X, simply"
+  echo "create an empty file called \'gc-aggressive-once\' in the \$SITE/git/X.git"
+  echo "folder:"
+  echo
+  echo "  \$ cd \$SITE/git/X.git"
+  echo "  \$ touch gc-aggressive-once"
+  echo
+  echo "On the next run, gc.sh will use --aggressive option for gc-ing this"
+  echo "repository *and* will remove this file. Next time, gc.sh again runs"
+  echo "normal gc for this repository."
+  echo
+  echo "To specify a permanent --aggressive git gc for a repository, create"
+  echo "an empty file named "gc-aggresssive" in the same folder:"
+  echo
+  echo "  \$ cd \$SITE/git/X.git"
+  echo "  \$ touch gc-aggressive"
+  echo
+  echo "Every next git gc on this repository will use --aggressive option."
+  exit 2
+}
+
+gc_options()
+{
+  if test -f "$1/gc-aggressive" ; then
+    echo "--aggressive"
+  elif test -f "$1/gc-aggressive-once" ; then
+    echo "--aggressive"
+    rm -f "$1/gc-aggressive-once"
+  else
+    echo ""
+  fi
+}
+
+log_opts()
+{
+  if test -z $1 ; then
+    echo ""
+  else
+    echo " [$1]"
+  fi
+}
+
+log()
+{
+  # Rotate the $LOG if current date is different from the last modification of $LOG
+  if test -f "$LOG" ; then
+    TODAY=$(date +%Y-%m-%d)
+    LOG_LAST_MODIFIED=$(date +%Y-%m-%d -r $LOG)
+    if test "$TODAY" != "$LOG_LAST_MODIFIED" ; then
+      mv "$LOG" "$LOG.$LOG_LAST_MODIFIED"
+      gzip "$LOG.$LOG_LAST_MODIFIED"
+    fi
+  fi
+
+  # Do not log an empty line
+  if [[ ! "$1" =~ ^[[:space:]]*$ ]]; then
+    echo $1
+    echo $1 >>$LOG
+  fi
+}
+
+gc_all_projects()
+{
+  find $TOP -type d -path "*.git" -prune -o -name "*.git" | while IFS= read d
+  do
+    gc_project "${d#$TOP/}"
+  done
+}
+
+gc_specified_projects()
+{
+  for PROJECT_NAME in ${GC_PROJECTS}
+  do
+    gc_project "$PROJECT_NAME"
+  done
+}
+
+gc_project()
+{
+  PROJECT_NAME="$@"
+  PROJECT_DIR="$TOP/$PROJECT_NAME"
+
+  if [[ ! -d "$PROJECT_DIR" ]]; then
+    OUT=$(date +"%D %r Failed: Directory does not exist: $PROJECT_DIR") && log "$OUT"
+    return 1
+  fi
+
+  OPTS=$(gc_options "$PROJECT_DIR")
+  LOG_OPTS=$(log_opts $OPTS)
+
+  # Check if git-gc for this project has to be skipped
+  if [ $SKIP_PROJECTS_OPT -eq 1 ]; then
+    for SKIP_PROJECT in "${SKIP_PROJECTS}"; do
+      if [ "$SKIP_PROJECT" == "$PROJECT_NAME" ] ; then
+        OUT=$(date +"%D %r Skipped: $PROJECT_NAME") && log "$OUT"
+        return 0
+      fi
+    done
+  fi
+
+  # Check if writing bitmaps for this project has to be disabled
+  WRITEBITMAPS='true';
+  if [ $DONOT_WRITE_BITMAPS_OPT -eq 1 ]; then
+    for BITMAP_PROJECT in "${DONOT_WRITE_BITMAPS}"; do
+      if [ "$BITMAP_PROJECT" == "$PROJECT_NAME" ] ; then
+        WRITEBITMAPS='false';
+      fi
+    done
+  fi
+
+  OUT=$(date +"%D %r Started: $PROJECT_NAME$LOG_OPTS") && log "$OUT"
+
+  git --git-dir="$PROJECT_DIR" config core.logallrefupdates true
+
+  git --git-dir="$PROJECT_DIR" config repack.usedeltabaseoffset true
+  git --git-dir="$PROJECT_DIR" config repack.writebitmaps $WRITEBITMAPS
+  git --git-dir="$PROJECT_DIR" config pack.compression 9
+  git --git-dir="$PROJECT_DIR" config pack.indexversion 2
+
+  git --git-dir="$PROJECT_DIR" config gc.autodetach false
+  git --git-dir="$PROJECT_DIR" config gc.auto 0
+  git --git-dir="$PROJECT_DIR" config gc.autopacklimit 0
+  git --git-dir="$PROJECT_DIR" config gc.packrefs true
+  git --git-dir="$PROJECT_DIR" config gc.reflogexpire never
+  git --git-dir="$PROJECT_DIR" config gc.reflogexpireunreachable never
+  git --git-dir="$PROJECT_DIR" config receive.autogc false
+
+  git --git-dir="$PROJECT_DIR" config pack.window 250
+  git --git-dir="$PROJECT_DIR" config pack.depth 50
+
+  OUT=$(git -c gc.auto=6700 -c gc.autoPackLimit=4 --git-dir="$PROJECT_DIR" gc --auto --prune $OPTS || date +"%D %r Failed: $PROJECT_NAME") \
+    && log "$OUT"
+
+  (find "$PROJECT_DIR/refs/changes" -type d | xargs rmdir;
+   find "$PROJECT_DIR/refs/changes" -type d | xargs rmdir
+  ) 2>/dev/null
+
+  OUT=$(date +"%D %r Finished: $PROJECT_NAME$LOG_OPTS") && log "$OUT"
+}
+
+###########################
+# Main script starts here #
+###########################
+
+SKIP_PROJECTS=
+GC_PROJECTS=
+DONOT_WRITE_BITMAPS=
+SKIP_PROJECTS_OPT=0
+GC_PROJECTS_OPT=0
+DONOT_WRITE_BITMAPS_OPT=0
+
+while getopts 's:p:b:?h' c
+do
+  case $c in
+    s)
+      SKIP_PROJECTS="${SKIP_PROJECTS} ${OPTARG}.git"
+      SKIP_PROJECTS_OPT=1
+      ;;
+    p)
+      GC_PROJECTS="${GC_PROJECTS} ${OPTARG}.git"
+      GC_PROJECTS_OPT=1
+      ;;
+    b)
+      DONOT_WRITE_BITMAPS="${DONOT_WRITE_BITMAPS} ${OPTARG}.git"
+      DONOT_WRITE_BITMAPS_OPT=1
+      ;;
+    h|?)
+      usage
+      ;;
+  esac
+done
+
+test $# -eq 0 || usage
+
+TOP=/var/gerrit/git
+LOG=/var/log/git/gc.log
+
+OUT=$(date +"%D %r Started") && log "$OUT"
+
+if [ $GC_PROJECTS_OPT -eq 1 ]; then
+    gc_specified_projects
+else
+    gc_all_projects
+fi
+
+OUT=$(date +"%D %r Finished") && log "$OUT"
+
+exit 0
diff --git a/charts/k8s-gerrit/container-images/publish_list b/charts/k8s-gerrit/container-images/publish_list
new file mode 100644
index 0000000..9c8ad64
--- /dev/null
+++ b/charts/k8s-gerrit/container-images/publish_list
@@ -0,0 +1,6 @@
+get_image_list(){
+  echo  "apache-git-http-backend" \
+        "gerrit-init" \
+        "gerrit" \
+        "git-gc"
+}
diff --git a/charts/k8s-gerrit/get_version.sh b/charts/k8s-gerrit/get_version.sh
new file mode 100755
index 0000000..df2dc0f
--- /dev/null
+++ b/charts/k8s-gerrit/get_version.sh
@@ -0,0 +1,5 @@
+REV=$(git describe --always --dirty)
+GERRIT_VERSION=$(docker run --platform=linux/amd64 --entrypoint "/bin/sh" gerrit-base:$REV \
+    -c "java -jar /var/gerrit/bin/gerrit.war version")
+GERRIT_VERSION=$(echo "${GERRIT_VERSION##*$'\n'}" | cut -d' ' -f3 | tr -d '[:space:]')
+echo "$REV-$GERRIT_VERSION"
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-operator-crds/.helmignore b/charts/k8s-gerrit/helm-charts/gerrit-operator-crds/.helmignore
new file mode 100644
index 0000000..0e8a0eb
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-operator-crds/.helmignore
@@ -0,0 +1,23 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*.orig
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
+.vscode/
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-operator-crds/Chart.yaml b/charts/k8s-gerrit/helm-charts/gerrit-operator-crds/Chart.yaml
new file mode 100644
index 0000000..9aa020c
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-operator-crds/Chart.yaml
@@ -0,0 +1,10 @@
+apiVersion: v2
+name: gerrit-operator-crds
+description: |
+  This helm chart installs CRDs that are managed/referenced by the gerrit
+  operator; namely - GerritCluster, Gerrit, GitGarbageCollection, Receiver. This
+  chart needs to be updated whenever there is a change in the operator source
+  code that updates the CRDs generated by fabric8.
+sources:
+- https://gerrit.googlesource.com/k8s-gerrit/+/refs/heads/master/operator
+version : 0.1.0
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-operator-crds/templates/gerritclusters.gerritoperator.google.com-v1.yml b/charts/k8s-gerrit/helm-charts/gerrit-operator-crds/templates/gerritclusters.gerritoperator.google.com-v1.yml
new file mode 100644
index 0000000..0fa15b7
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-operator-crds/templates/gerritclusters.gerritoperator.google.com-v1.yml
@@ -0,0 +1,1494 @@
+# Generated by Fabric8 CRDGenerator, manual edits might get overwritten!
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  name: gerritclusters.gerritoperator.google.com
+spec:
+  group: gerritoperator.google.com
+  names:
+    kind: GerritCluster
+    plural: gerritclusters
+    shortNames:
+    - gclus
+    singular: gerritcluster
+  scope: Namespaced
+  versions:
+  - name: v1alpha17
+    schema:
+      openAPIV3Schema:
+        properties:
+          spec:
+            properties:
+              storage:
+                properties:
+                  pluginCache:
+                    properties:
+                      enabled:
+                        type: boolean
+                    type: object
+                  storageClasses:
+                    properties:
+                      readWriteOnce:
+                        type: string
+                      readWriteMany:
+                        type: string
+                      nfsWorkaround:
+                        properties:
+                          enabled:
+                            type: boolean
+                          chownOnStartup:
+                            type: boolean
+                          idmapdConfig:
+                            type: string
+                        type: object
+                    type: object
+                  sharedStorage:
+                    properties:
+                      externalPVC:
+                        properties:
+                          enabled:
+                            type: boolean
+                          claimName:
+                            type: string
+                        type: object
+                      size:
+                        anyOf:
+                        - type: integer
+                        - type: string
+                        x-kubernetes-int-or-string: true
+                      volumeName:
+                        type: string
+                      selector:
+                        properties:
+                          matchExpressions:
+                            items:
+                              properties:
+                                key:
+                                  type: string
+                                operator:
+                                  type: string
+                                values:
+                                  items:
+                                    type: string
+                                  type: array
+                              type: object
+                            type: array
+                          matchLabels:
+                            additionalProperties:
+                              type: string
+                            type: object
+                        type: object
+                    type: object
+                type: object
+              containerImages:
+                properties:
+                  imagePullPolicy:
+                    type: string
+                  imagePullSecrets:
+                    items:
+                      properties:
+                        name:
+                          type: string
+                      type: object
+                    type: array
+                  busyBox:
+                    properties:
+                      registry:
+                        type: string
+                      tag:
+                        type: string
+                    type: object
+                  gerritImages:
+                    properties:
+                      registry:
+                        type: string
+                      org:
+                        type: string
+                      tag:
+                        type: string
+                    type: object
+                type: object
+              ingress:
+                properties:
+                  enabled:
+                    type: boolean
+                  host:
+                    type: string
+                  annotations:
+                    additionalProperties:
+                      type: string
+                    type: object
+                  tls:
+                    properties:
+                      enabled:
+                        type: boolean
+                      secret:
+                        type: string
+                    type: object
+                  ssh:
+                    properties:
+                      enabled:
+                        type: boolean
+                    type: object
+                  ambassador:
+                    properties:
+                      id:
+                        items:
+                          type: string
+                        type: array
+                      createHost:
+                        type: boolean
+                    type: object
+                type: object
+              refdb:
+                properties:
+                  database:
+                    enum:
+                    - NONE
+                    - ZOOKEEPER
+                    - SPANNER
+                    type: string
+                  zookeeper:
+                    properties:
+                      connectString:
+                        type: string
+                      rootNode:
+                        type: string
+                    type: object
+                  spanner:
+                    properties:
+                      projectName:
+                        type: string
+                      instance:
+                        type: string
+                      database:
+                        type: string
+                    type: object
+                type: object
+              serverId:
+                type: string
+              gerrits:
+                items:
+                  properties:
+                    metadata:
+                      properties:
+                        annotations:
+                          additionalProperties:
+                            type: string
+                          type: object
+                        creationTimestamp:
+                          type: string
+                        deletionGracePeriodSeconds:
+                          type: integer
+                        deletionTimestamp:
+                          type: string
+                        finalizers:
+                          items:
+                            type: string
+                          type: array
+                        generateName:
+                          type: string
+                        generation:
+                          type: integer
+                        labels:
+                          additionalProperties:
+                            type: string
+                          type: object
+                        managedFields:
+                          items:
+                            properties:
+                              apiVersion:
+                                type: string
+                              fieldsType:
+                                type: string
+                              fieldsV1:
+                                type: object
+                              manager:
+                                type: string
+                              operation:
+                                type: string
+                              subresource:
+                                type: string
+                              time:
+                                type: string
+                            type: object
+                          type: array
+                        name:
+                          type: string
+                        namespace:
+                          type: string
+                        ownerReferences:
+                          items:
+                            properties:
+                              apiVersion:
+                                type: string
+                              blockOwnerDeletion:
+                                type: boolean
+                              controller:
+                                type: boolean
+                              kind:
+                                type: string
+                              name:
+                                type: string
+                              uid:
+                                type: string
+                            type: object
+                          type: array
+                        resourceVersion:
+                          type: string
+                        selfLink:
+                          type: string
+                        uid:
+                          type: string
+                      type: object
+                    spec:
+                      properties:
+                        serviceAccount:
+                          type: string
+                        tolerations:
+                          items:
+                            properties:
+                              effect:
+                                type: string
+                              key:
+                                type: string
+                              operator:
+                                type: string
+                              tolerationSeconds:
+                                type: integer
+                              value:
+                                type: string
+                            type: object
+                          type: array
+                        affinity:
+                          properties:
+                            nodeAffinity:
+                              properties:
+                                preferredDuringSchedulingIgnoredDuringExecution:
+                                  items:
+                                    properties:
+                                      preference:
+                                        properties:
+                                          matchExpressions:
+                                            items:
+                                              properties:
+                                                key:
+                                                  type: string
+                                                operator:
+                                                  type: string
+                                                values:
+                                                  items:
+                                                    type: string
+                                                  type: array
+                                              type: object
+                                            type: array
+                                          matchFields:
+                                            items:
+                                              properties:
+                                                key:
+                                                  type: string
+                                                operator:
+                                                  type: string
+                                                values:
+                                                  items:
+                                                    type: string
+                                                  type: array
+                                              type: object
+                                            type: array
+                                        type: object
+                                      weight:
+                                        type: integer
+                                    type: object
+                                  type: array
+                                requiredDuringSchedulingIgnoredDuringExecution:
+                                  properties:
+                                    nodeSelectorTerms:
+                                      items:
+                                        properties:
+                                          matchExpressions:
+                                            items:
+                                              properties:
+                                                key:
+                                                  type: string
+                                                operator:
+                                                  type: string
+                                                values:
+                                                  items:
+                                                    type: string
+                                                  type: array
+                                              type: object
+                                            type: array
+                                          matchFields:
+                                            items:
+                                              properties:
+                                                key:
+                                                  type: string
+                                                operator:
+                                                  type: string
+                                                values:
+                                                  items:
+                                                    type: string
+                                                  type: array
+                                              type: object
+                                            type: array
+                                        type: object
+                                      type: array
+                                  type: object
+                              type: object
+                            podAffinity:
+                              properties:
+                                preferredDuringSchedulingIgnoredDuringExecution:
+                                  items:
+                                    properties:
+                                      podAffinityTerm:
+                                        properties:
+                                          labelSelector:
+                                            properties:
+                                              matchExpressions:
+                                                items:
+                                                  properties:
+                                                    key:
+                                                      type: string
+                                                    operator:
+                                                      type: string
+                                                    values:
+                                                      items:
+                                                        type: string
+                                                      type: array
+                                                  type: object
+                                                type: array
+                                              matchLabels:
+                                                additionalProperties:
+                                                  type: string
+                                                type: object
+                                            type: object
+                                          namespaceSelector:
+                                            properties:
+                                              matchExpressions:
+                                                items:
+                                                  properties:
+                                                    key:
+                                                      type: string
+                                                    operator:
+                                                      type: string
+                                                    values:
+                                                      items:
+                                                        type: string
+                                                      type: array
+                                                  type: object
+                                                type: array
+                                              matchLabels:
+                                                additionalProperties:
+                                                  type: string
+                                                type: object
+                                            type: object
+                                          namespaces:
+                                            items:
+                                              type: string
+                                            type: array
+                                          topologyKey:
+                                            type: string
+                                        type: object
+                                      weight:
+                                        type: integer
+                                    type: object
+                                  type: array
+                                requiredDuringSchedulingIgnoredDuringExecution:
+                                  items:
+                                    properties:
+                                      labelSelector:
+                                        properties:
+                                          matchExpressions:
+                                            items:
+                                              properties:
+                                                key:
+                                                  type: string
+                                                operator:
+                                                  type: string
+                                                values:
+                                                  items:
+                                                    type: string
+                                                  type: array
+                                              type: object
+                                            type: array
+                                          matchLabels:
+                                            additionalProperties:
+                                              type: string
+                                            type: object
+                                        type: object
+                                      namespaceSelector:
+                                        properties:
+                                          matchExpressions:
+                                            items:
+                                              properties:
+                                                key:
+                                                  type: string
+                                                operator:
+                                                  type: string
+                                                values:
+                                                  items:
+                                                    type: string
+                                                  type: array
+                                              type: object
+                                            type: array
+                                          matchLabels:
+                                            additionalProperties:
+                                              type: string
+                                            type: object
+                                        type: object
+                                      namespaces:
+                                        items:
+                                          type: string
+                                        type: array
+                                      topologyKey:
+                                        type: string
+                                    type: object
+                                  type: array
+                              type: object
+                            podAntiAffinity:
+                              properties:
+                                preferredDuringSchedulingIgnoredDuringExecution:
+                                  items:
+                                    properties:
+                                      podAffinityTerm:
+                                        properties:
+                                          labelSelector:
+                                            properties:
+                                              matchExpressions:
+                                                items:
+                                                  properties:
+                                                    key:
+                                                      type: string
+                                                    operator:
+                                                      type: string
+                                                    values:
+                                                      items:
+                                                        type: string
+                                                      type: array
+                                                  type: object
+                                                type: array
+                                              matchLabels:
+                                                additionalProperties:
+                                                  type: string
+                                                type: object
+                                            type: object
+                                          namespaceSelector:
+                                            properties:
+                                              matchExpressions:
+                                                items:
+                                                  properties:
+                                                    key:
+                                                      type: string
+                                                    operator:
+                                                      type: string
+                                                    values:
+                                                      items:
+                                                        type: string
+                                                      type: array
+                                                  type: object
+                                                type: array
+                                              matchLabels:
+                                                additionalProperties:
+                                                  type: string
+                                                type: object
+                                            type: object
+                                          namespaces:
+                                            items:
+                                              type: string
+                                            type: array
+                                          topologyKey:
+                                            type: string
+                                        type: object
+                                      weight:
+                                        type: integer
+                                    type: object
+                                  type: array
+                                requiredDuringSchedulingIgnoredDuringExecution:
+                                  items:
+                                    properties:
+                                      labelSelector:
+                                        properties:
+                                          matchExpressions:
+                                            items:
+                                              properties:
+                                                key:
+                                                  type: string
+                                                operator:
+                                                  type: string
+                                                values:
+                                                  items:
+                                                    type: string
+                                                  type: array
+                                              type: object
+                                            type: array
+                                          matchLabels:
+                                            additionalProperties:
+                                              type: string
+                                            type: object
+                                        type: object
+                                      namespaceSelector:
+                                        properties:
+                                          matchExpressions:
+                                            items:
+                                              properties:
+                                                key:
+                                                  type: string
+                                                operator:
+                                                  type: string
+                                                values:
+                                                  items:
+                                                    type: string
+                                                  type: array
+                                              type: object
+                                            type: array
+                                          matchLabels:
+                                            additionalProperties:
+                                              type: string
+                                            type: object
+                                        type: object
+                                      namespaces:
+                                        items:
+                                          type: string
+                                        type: array
+                                      topologyKey:
+                                        type: string
+                                    type: object
+                                  type: array
+                              type: object
+                          type: object
+                        topologySpreadConstraints:
+                          items:
+                            properties:
+                              labelSelector:
+                                properties:
+                                  matchExpressions:
+                                    items:
+                                      properties:
+                                        key:
+                                          type: string
+                                        operator:
+                                          type: string
+                                        values:
+                                          items:
+                                            type: string
+                                          type: array
+                                      type: object
+                                    type: array
+                                  matchLabels:
+                                    additionalProperties:
+                                      type: string
+                                    type: object
+                                type: object
+                              matchLabelKeys:
+                                items:
+                                  type: string
+                                type: array
+                              maxSkew:
+                                type: integer
+                              minDomains:
+                                type: integer
+                              nodeAffinityPolicy:
+                                type: string
+                              nodeTaintsPolicy:
+                                type: string
+                              topologyKey:
+                                type: string
+                              whenUnsatisfiable:
+                                type: string
+                            type: object
+                          type: array
+                        priorityClassName:
+                          type: string
+                        replicas:
+                          type: integer
+                        updatePartition:
+                          type: integer
+                        resources:
+                          properties:
+                            claims:
+                              items:
+                                properties:
+                                  name:
+                                    type: string
+                                type: object
+                              type: array
+                            limits:
+                              additionalProperties:
+                                anyOf:
+                                - type: integer
+                                - type: string
+                                x-kubernetes-int-or-string: true
+                              type: object
+                            requests:
+                              additionalProperties:
+                                anyOf:
+                                - type: integer
+                                - type: string
+                                x-kubernetes-int-or-string: true
+                              type: object
+                          type: object
+                        startupProbe:
+                          properties:
+                            exec:
+                              properties:
+                                command:
+                                  items:
+                                    type: string
+                                  type: array
+                              type: object
+                            failureThreshold:
+                              type: integer
+                            grpc:
+                              properties:
+                                port:
+                                  type: integer
+                                service:
+                                  type: string
+                              type: object
+                            httpGet:
+                              properties:
+                                host:
+                                  type: string
+                                httpHeaders:
+                                  items:
+                                    properties:
+                                      name:
+                                        type: string
+                                      value:
+                                        type: string
+                                    type: object
+                                  type: array
+                                path:
+                                  type: string
+                                port:
+                                  anyOf:
+                                  - type: integer
+                                  - type: string
+                                  x-kubernetes-int-or-string: true
+                                scheme:
+                                  type: string
+                              type: object
+                            initialDelaySeconds:
+                              type: integer
+                            periodSeconds:
+                              type: integer
+                            successThreshold:
+                              type: integer
+                            tcpSocket:
+                              properties:
+                                host:
+                                  type: string
+                                port:
+                                  anyOf:
+                                  - type: integer
+                                  - type: string
+                                  x-kubernetes-int-or-string: true
+                              type: object
+                            terminationGracePeriodSeconds:
+                              type: integer
+                            timeoutSeconds:
+                              type: integer
+                          type: object
+                        readinessProbe:
+                          properties:
+                            exec:
+                              properties:
+                                command:
+                                  items:
+                                    type: string
+                                  type: array
+                              type: object
+                            failureThreshold:
+                              type: integer
+                            grpc:
+                              properties:
+                                port:
+                                  type: integer
+                                service:
+                                  type: string
+                              type: object
+                            httpGet:
+                              properties:
+                                host:
+                                  type: string
+                                httpHeaders:
+                                  items:
+                                    properties:
+                                      name:
+                                        type: string
+                                      value:
+                                        type: string
+                                    type: object
+                                  type: array
+                                path:
+                                  type: string
+                                port:
+                                  anyOf:
+                                  - type: integer
+                                  - type: string
+                                  x-kubernetes-int-or-string: true
+                                scheme:
+                                  type: string
+                              type: object
+                            initialDelaySeconds:
+                              type: integer
+                            periodSeconds:
+                              type: integer
+                            successThreshold:
+                              type: integer
+                            tcpSocket:
+                              properties:
+                                host:
+                                  type: string
+                                port:
+                                  anyOf:
+                                  - type: integer
+                                  - type: string
+                                  x-kubernetes-int-or-string: true
+                              type: object
+                            terminationGracePeriodSeconds:
+                              type: integer
+                            timeoutSeconds:
+                              type: integer
+                          type: object
+                        livenessProbe:
+                          properties:
+                            exec:
+                              properties:
+                                command:
+                                  items:
+                                    type: string
+                                  type: array
+                              type: object
+                            failureThreshold:
+                              type: integer
+                            grpc:
+                              properties:
+                                port:
+                                  type: integer
+                                service:
+                                  type: string
+                              type: object
+                            httpGet:
+                              properties:
+                                host:
+                                  type: string
+                                httpHeaders:
+                                  items:
+                                    properties:
+                                      name:
+                                        type: string
+                                      value:
+                                        type: string
+                                    type: object
+                                  type: array
+                                path:
+                                  type: string
+                                port:
+                                  anyOf:
+                                  - type: integer
+                                  - type: string
+                                  x-kubernetes-int-or-string: true
+                                scheme:
+                                  type: string
+                              type: object
+                            initialDelaySeconds:
+                              type: integer
+                            periodSeconds:
+                              type: integer
+                            successThreshold:
+                              type: integer
+                            tcpSocket:
+                              properties:
+                                host:
+                                  type: string
+                                port:
+                                  anyOf:
+                                  - type: integer
+                                  - type: string
+                                  x-kubernetes-int-or-string: true
+                              type: object
+                            terminationGracePeriodSeconds:
+                              type: integer
+                            timeoutSeconds:
+                              type: integer
+                          type: object
+                        gracefulStopTimeout:
+                          type: integer
+                        service:
+                          properties:
+                            sshPort:
+                              type: integer
+                            type:
+                              type: string
+                            httpPort:
+                              type: integer
+                          type: object
+                        site:
+                          properties:
+                            size:
+                              anyOf:
+                              - type: integer
+                              - type: string
+                              x-kubernetes-int-or-string: true
+                          type: object
+                        plugins:
+                          items:
+                            properties:
+                              installAsLibrary:
+                                type: boolean
+                              name:
+                                type: string
+                              url:
+                                type: string
+                              sha1:
+                                type: string
+                            type: object
+                          type: array
+                        libs:
+                          items:
+                            properties:
+                              name:
+                                type: string
+                              url:
+                                type: string
+                              sha1:
+                                type: string
+                            type: object
+                          type: array
+                        configFiles:
+                          additionalProperties:
+                            type: string
+                          type: object
+                        secretRef:
+                          type: string
+                        mode:
+                          enum:
+                          - PRIMARY
+                          - REPLICA
+                          type: string
+                        debug:
+                          properties:
+                            enabled:
+                              type: boolean
+                            suspend:
+                              type: boolean
+                          type: object
+                      type: object
+                  type: object
+                type: array
+              receiver:
+                properties:
+                  metadata:
+                    properties:
+                      annotations:
+                        additionalProperties:
+                          type: string
+                        type: object
+                      creationTimestamp:
+                        type: string
+                      deletionGracePeriodSeconds:
+                        type: integer
+                      deletionTimestamp:
+                        type: string
+                      finalizers:
+                        items:
+                          type: string
+                        type: array
+                      generateName:
+                        type: string
+                      generation:
+                        type: integer
+                      labels:
+                        additionalProperties:
+                          type: string
+                        type: object
+                      managedFields:
+                        items:
+                          properties:
+                            apiVersion:
+                              type: string
+                            fieldsType:
+                              type: string
+                            fieldsV1:
+                              type: object
+                            manager:
+                              type: string
+                            operation:
+                              type: string
+                            subresource:
+                              type: string
+                            time:
+                              type: string
+                          type: object
+                        type: array
+                      name:
+                        type: string
+                      namespace:
+                        type: string
+                      ownerReferences:
+                        items:
+                          properties:
+                            apiVersion:
+                              type: string
+                            blockOwnerDeletion:
+                              type: boolean
+                            controller:
+                              type: boolean
+                            kind:
+                              type: string
+                            name:
+                              type: string
+                            uid:
+                              type: string
+                          type: object
+                        type: array
+                      resourceVersion:
+                        type: string
+                      selfLink:
+                        type: string
+                      uid:
+                        type: string
+                    type: object
+                  spec:
+                    properties:
+                      tolerations:
+                        items:
+                          properties:
+                            effect:
+                              type: string
+                            key:
+                              type: string
+                            operator:
+                              type: string
+                            tolerationSeconds:
+                              type: integer
+                            value:
+                              type: string
+                          type: object
+                        type: array
+                      affinity:
+                        properties:
+                          nodeAffinity:
+                            properties:
+                              preferredDuringSchedulingIgnoredDuringExecution:
+                                items:
+                                  properties:
+                                    preference:
+                                      properties:
+                                        matchExpressions:
+                                          items:
+                                            properties:
+                                              key:
+                                                type: string
+                                              operator:
+                                                type: string
+                                              values:
+                                                items:
+                                                  type: string
+                                                type: array
+                                            type: object
+                                          type: array
+                                        matchFields:
+                                          items:
+                                            properties:
+                                              key:
+                                                type: string
+                                              operator:
+                                                type: string
+                                              values:
+                                                items:
+                                                  type: string
+                                                type: array
+                                            type: object
+                                          type: array
+                                      type: object
+                                    weight:
+                                      type: integer
+                                  type: object
+                                type: array
+                              requiredDuringSchedulingIgnoredDuringExecution:
+                                properties:
+                                  nodeSelectorTerms:
+                                    items:
+                                      properties:
+                                        matchExpressions:
+                                          items:
+                                            properties:
+                                              key:
+                                                type: string
+                                              operator:
+                                                type: string
+                                              values:
+                                                items:
+                                                  type: string
+                                                type: array
+                                            type: object
+                                          type: array
+                                        matchFields:
+                                          items:
+                                            properties:
+                                              key:
+                                                type: string
+                                              operator:
+                                                type: string
+                                              values:
+                                                items:
+                                                  type: string
+                                                type: array
+                                            type: object
+                                          type: array
+                                      type: object
+                                    type: array
+                                type: object
+                            type: object
+                          podAffinity:
+                            properties:
+                              preferredDuringSchedulingIgnoredDuringExecution:
+                                items:
+                                  properties:
+                                    podAffinityTerm:
+                                      properties:
+                                        labelSelector:
+                                          properties:
+                                            matchExpressions:
+                                              items:
+                                                properties:
+                                                  key:
+                                                    type: string
+                                                  operator:
+                                                    type: string
+                                                  values:
+                                                    items:
+                                                      type: string
+                                                    type: array
+                                                type: object
+                                              type: array
+                                            matchLabels:
+                                              additionalProperties:
+                                                type: string
+                                              type: object
+                                          type: object
+                                        namespaceSelector:
+                                          properties:
+                                            matchExpressions:
+                                              items:
+                                                properties:
+                                                  key:
+                                                    type: string
+                                                  operator:
+                                                    type: string
+                                                  values:
+                                                    items:
+                                                      type: string
+                                                    type: array
+                                                type: object
+                                              type: array
+                                            matchLabels:
+                                              additionalProperties:
+                                                type: string
+                                              type: object
+                                          type: object
+                                        namespaces:
+                                          items:
+                                            type: string
+                                          type: array
+                                        topologyKey:
+                                          type: string
+                                      type: object
+                                    weight:
+                                      type: integer
+                                  type: object
+                                type: array
+                              requiredDuringSchedulingIgnoredDuringExecution:
+                                items:
+                                  properties:
+                                    labelSelector:
+                                      properties:
+                                        matchExpressions:
+                                          items:
+                                            properties:
+                                              key:
+                                                type: string
+                                              operator:
+                                                type: string
+                                              values:
+                                                items:
+                                                  type: string
+                                                type: array
+                                            type: object
+                                          type: array
+                                        matchLabels:
+                                          additionalProperties:
+                                            type: string
+                                          type: object
+                                      type: object
+                                    namespaceSelector:
+                                      properties:
+                                        matchExpressions:
+                                          items:
+                                            properties:
+                                              key:
+                                                type: string
+                                              operator:
+                                                type: string
+                                              values:
+                                                items:
+                                                  type: string
+                                                type: array
+                                            type: object
+                                          type: array
+                                        matchLabels:
+                                          additionalProperties:
+                                            type: string
+                                          type: object
+                                      type: object
+                                    namespaces:
+                                      items:
+                                        type: string
+                                      type: array
+                                    topologyKey:
+                                      type: string
+                                  type: object
+                                type: array
+                            type: object
+                          podAntiAffinity:
+                            properties:
+                              preferredDuringSchedulingIgnoredDuringExecution:
+                                items:
+                                  properties:
+                                    podAffinityTerm:
+                                      properties:
+                                        labelSelector:
+                                          properties:
+                                            matchExpressions:
+                                              items:
+                                                properties:
+                                                  key:
+                                                    type: string
+                                                  operator:
+                                                    type: string
+                                                  values:
+                                                    items:
+                                                      type: string
+                                                    type: array
+                                                type: object
+                                              type: array
+                                            matchLabels:
+                                              additionalProperties:
+                                                type: string
+                                              type: object
+                                          type: object
+                                        namespaceSelector:
+                                          properties:
+                                            matchExpressions:
+                                              items:
+                                                properties:
+                                                  key:
+                                                    type: string
+                                                  operator:
+                                                    type: string
+                                                  values:
+                                                    items:
+                                                      type: string
+                                                    type: array
+                                                type: object
+                                              type: array
+                                            matchLabels:
+                                              additionalProperties:
+                                                type: string
+                                              type: object
+                                          type: object
+                                        namespaces:
+                                          items:
+                                            type: string
+                                          type: array
+                                        topologyKey:
+                                          type: string
+                                      type: object
+                                    weight:
+                                      type: integer
+                                  type: object
+                                type: array
+                              requiredDuringSchedulingIgnoredDuringExecution:
+                                items:
+                                  properties:
+                                    labelSelector:
+                                      properties:
+                                        matchExpressions:
+                                          items:
+                                            properties:
+                                              key:
+                                                type: string
+                                              operator:
+                                                type: string
+                                              values:
+                                                items:
+                                                  type: string
+                                                type: array
+                                            type: object
+                                          type: array
+                                        matchLabels:
+                                          additionalProperties:
+                                            type: string
+                                          type: object
+                                      type: object
+                                    namespaceSelector:
+                                      properties:
+                                        matchExpressions:
+                                          items:
+                                            properties:
+                                              key:
+                                                type: string
+                                              operator:
+                                                type: string
+                                              values:
+                                                items:
+                                                  type: string
+                                                type: array
+                                            type: object
+                                          type: array
+                                        matchLabels:
+                                          additionalProperties:
+                                            type: string
+                                          type: object
+                                      type: object
+                                    namespaces:
+                                      items:
+                                        type: string
+                                      type: array
+                                    topologyKey:
+                                      type: string
+                                  type: object
+                                type: array
+                            type: object
+                        type: object
+                      topologySpreadConstraints:
+                        items:
+                          properties:
+                            labelSelector:
+                              properties:
+                                matchExpressions:
+                                  items:
+                                    properties:
+                                      key:
+                                        type: string
+                                      operator:
+                                        type: string
+                                      values:
+                                        items:
+                                          type: string
+                                        type: array
+                                    type: object
+                                  type: array
+                                matchLabels:
+                                  additionalProperties:
+                                    type: string
+                                  type: object
+                              type: object
+                            matchLabelKeys:
+                              items:
+                                type: string
+                              type: array
+                            maxSkew:
+                              type: integer
+                            minDomains:
+                              type: integer
+                            nodeAffinityPolicy:
+                              type: string
+                            nodeTaintsPolicy:
+                              type: string
+                            topologyKey:
+                              type: string
+                            whenUnsatisfiable:
+                              type: string
+                          type: object
+                        type: array
+                      priorityClassName:
+                        type: string
+                      replicas:
+                        type: integer
+                      maxSurge:
+                        anyOf:
+                        - type: integer
+                        - type: string
+                        x-kubernetes-int-or-string: true
+                      maxUnavailable:
+                        anyOf:
+                        - type: integer
+                        - type: string
+                        x-kubernetes-int-or-string: true
+                      resources:
+                        properties:
+                          claims:
+                            items:
+                              properties:
+                                name:
+                                  type: string
+                              type: object
+                            type: array
+                          limits:
+                            additionalProperties:
+                              anyOf:
+                              - type: integer
+                              - type: string
+                              x-kubernetes-int-or-string: true
+                            type: object
+                          requests:
+                            additionalProperties:
+                              anyOf:
+                              - type: integer
+                              - type: string
+                              x-kubernetes-int-or-string: true
+                            type: object
+                        type: object
+                      readinessProbe:
+                        properties:
+                          exec:
+                            properties:
+                              command:
+                                items:
+                                  type: string
+                                type: array
+                            type: object
+                          failureThreshold:
+                            type: integer
+                          grpc:
+                            properties:
+                              port:
+                                type: integer
+                              service:
+                                type: string
+                            type: object
+                          httpGet:
+                            properties:
+                              host:
+                                type: string
+                              httpHeaders:
+                                items:
+                                  properties:
+                                    name:
+                                      type: string
+                                    value:
+                                      type: string
+                                  type: object
+                                type: array
+                              path:
+                                type: string
+                              port:
+                                anyOf:
+                                - type: integer
+                                - type: string
+                                x-kubernetes-int-or-string: true
+                              scheme:
+                                type: string
+                            type: object
+                          initialDelaySeconds:
+                            type: integer
+                          periodSeconds:
+                            type: integer
+                          successThreshold:
+                            type: integer
+                          tcpSocket:
+                            properties:
+                              host:
+                                type: string
+                              port:
+                                anyOf:
+                                - type: integer
+                                - type: string
+                                x-kubernetes-int-or-string: true
+                            type: object
+                          terminationGracePeriodSeconds:
+                            type: integer
+                          timeoutSeconds:
+                            type: integer
+                        type: object
+                      livenessProbe:
+                        properties:
+                          exec:
+                            properties:
+                              command:
+                                items:
+                                  type: string
+                                type: array
+                            type: object
+                          failureThreshold:
+                            type: integer
+                          grpc:
+                            properties:
+                              port:
+                                type: integer
+                              service:
+                                type: string
+                            type: object
+                          httpGet:
+                            properties:
+                              host:
+                                type: string
+                              httpHeaders:
+                                items:
+                                  properties:
+                                    name:
+                                      type: string
+                                    value:
+                                      type: string
+                                  type: object
+                                type: array
+                              path:
+                                type: string
+                              port:
+                                anyOf:
+                                - type: integer
+                                - type: string
+                                x-kubernetes-int-or-string: true
+                              scheme:
+                                type: string
+                            type: object
+                          initialDelaySeconds:
+                            type: integer
+                          periodSeconds:
+                            type: integer
+                          successThreshold:
+                            type: integer
+                          tcpSocket:
+                            properties:
+                              host:
+                                type: string
+                              port:
+                                anyOf:
+                                - type: integer
+                                - type: string
+                                x-kubernetes-int-or-string: true
+                            type: object
+                          terminationGracePeriodSeconds:
+                            type: integer
+                          timeoutSeconds:
+                            type: integer
+                        type: object
+                      service:
+                        properties:
+                          type:
+                            type: string
+                          httpPort:
+                            type: integer
+                        type: object
+                      credentialSecretRef:
+                        type: string
+                    type: object
+                type: object
+            type: object
+          status:
+            properties:
+              members:
+                additionalProperties:
+                  items:
+                    type: string
+                  type: array
+                type: object
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-operator-crds/templates/gerritnetworks.gerritoperator.google.com-v1.yml b/charts/k8s-gerrit/helm-charts/gerrit-operator-crds/templates/gerritnetworks.gerritoperator.google.com-v1.yml
new file mode 100644
index 0000000..77ac756
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-operator-crds/templates/gerritnetworks.gerritoperator.google.com-v1.yml
@@ -0,0 +1,134 @@
+# Generated by Fabric8 CRDGenerator, manual edits might get overwritten!
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  name: gerritnetworks.gerritoperator.google.com
+spec:
+  group: gerritoperator.google.com
+  names:
+    kind: GerritNetwork
+    plural: gerritnetworks
+    shortNames:
+    - gn
+    singular: gerritnetwork
+  scope: Namespaced
+  versions:
+  - name: v1alpha2
+    schema:
+      openAPIV3Schema:
+        properties:
+          spec:
+            properties:
+              ingress:
+                properties:
+                  enabled:
+                    type: boolean
+                  host:
+                    type: string
+                  annotations:
+                    additionalProperties:
+                      type: string
+                    type: object
+                  tls:
+                    properties:
+                      enabled:
+                        type: boolean
+                      secret:
+                        type: string
+                    type: object
+                  ssh:
+                    properties:
+                      enabled:
+                        type: boolean
+                    type: object
+                  ambassador:
+                    properties:
+                      id:
+                        items:
+                          type: string
+                        type: array
+                      createHost:
+                        type: boolean
+                    type: object
+                type: object
+              receiver:
+                properties:
+                  name:
+                    type: string
+                  httpPort:
+                    type: integer
+                type: object
+              primaryGerrit:
+                properties:
+                  sshPort:
+                    type: integer
+                  name:
+                    type: string
+                  httpPort:
+                    type: integer
+                type: object
+              gerritReplica:
+                properties:
+                  sshPort:
+                    type: integer
+                  name:
+                    type: string
+                  httpPort:
+                    type: integer
+                type: object
+            type: object
+          status:
+            properties:
+              apiVersion:
+                type: string
+              code:
+                type: integer
+              details:
+                properties:
+                  causes:
+                    items:
+                      properties:
+                        field:
+                          type: string
+                        message:
+                          type: string
+                        reason:
+                          type: string
+                      type: object
+                    type: array
+                  group:
+                    type: string
+                  kind:
+                    type: string
+                  name:
+                    type: string
+                  retryAfterSeconds:
+                    type: integer
+                  uid:
+                    type: string
+                type: object
+              kind:
+                type: string
+              message:
+                type: string
+              metadata:
+                properties:
+                  continue:
+                    type: string
+                  remainingItemCount:
+                    type: integer
+                  resourceVersion:
+                    type: string
+                  selfLink:
+                    type: string
+                type: object
+              reason:
+                type: string
+              status:
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-operator-crds/templates/gerrits.gerritoperator.google.com-v1.yml b/charts/k8s-gerrit/helm-charts/gerrit-operator-crds/templates/gerrits.gerritoperator.google.com-v1.yml
new file mode 100644
index 0000000..a029288
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-operator-crds/templates/gerrits.gerritoperator.google.com-v1.yml
@@ -0,0 +1,801 @@
+# Generated by Fabric8 CRDGenerator, manual edits might get overwritten!
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  name: gerrits.gerritoperator.google.com
+spec:
+  group: gerritoperator.google.com
+  names:
+    kind: Gerrit
+    plural: gerrits
+    shortNames:
+    - gcr
+    singular: gerrit
+  scope: Namespaced
+  versions:
+  - name: v1alpha17
+    schema:
+      openAPIV3Schema:
+        properties:
+          spec:
+            properties:
+              containerImages:
+                properties:
+                  imagePullPolicy:
+                    type: string
+                  imagePullSecrets:
+                    items:
+                      properties:
+                        name:
+                          type: string
+                      type: object
+                    type: array
+                  busyBox:
+                    properties:
+                      registry:
+                        type: string
+                      tag:
+                        type: string
+                    type: object
+                  gerritImages:
+                    properties:
+                      registry:
+                        type: string
+                      org:
+                        type: string
+                      tag:
+                        type: string
+                    type: object
+                type: object
+              storage:
+                properties:
+                  pluginCache:
+                    properties:
+                      enabled:
+                        type: boolean
+                    type: object
+                  storageClasses:
+                    properties:
+                      readWriteOnce:
+                        type: string
+                      readWriteMany:
+                        type: string
+                      nfsWorkaround:
+                        properties:
+                          enabled:
+                            type: boolean
+                          chownOnStartup:
+                            type: boolean
+                          idmapdConfig:
+                            type: string
+                        type: object
+                    type: object
+                  sharedStorage:
+                    properties:
+                      externalPVC:
+                        properties:
+                          enabled:
+                            type: boolean
+                          claimName:
+                            type: string
+                        type: object
+                      size:
+                        anyOf:
+                        - type: integer
+                        - type: string
+                        x-kubernetes-int-or-string: true
+                      volumeName:
+                        type: string
+                      selector:
+                        properties:
+                          matchExpressions:
+                            items:
+                              properties:
+                                key:
+                                  type: string
+                                operator:
+                                  type: string
+                                values:
+                                  items:
+                                    type: string
+                                  type: array
+                              type: object
+                            type: array
+                          matchLabels:
+                            additionalProperties:
+                              type: string
+                            type: object
+                        type: object
+                    type: object
+                type: object
+              ingress:
+                properties:
+                  enabled:
+                    type: boolean
+                  host:
+                    type: string
+                  tlsEnabled:
+                    type: boolean
+                  ssh:
+                    properties:
+                      enabled:
+                        type: boolean
+                    type: object
+                type: object
+              refdb:
+                properties:
+                  database:
+                    enum:
+                    - NONE
+                    - ZOOKEEPER
+                    - SPANNER
+                    type: string
+                  zookeeper:
+                    properties:
+                      connectString:
+                        type: string
+                      rootNode:
+                        type: string
+                    type: object
+                  spanner:
+                    properties:
+                      projectName:
+                        type: string
+                      instance:
+                        type: string
+                      database:
+                        type: string
+                    type: object
+                type: object
+              serverId:
+                type: string
+              serviceAccount:
+                type: string
+              tolerations:
+                items:
+                  properties:
+                    effect:
+                      type: string
+                    key:
+                      type: string
+                    operator:
+                      type: string
+                    tolerationSeconds:
+                      type: integer
+                    value:
+                      type: string
+                  type: object
+                type: array
+              affinity:
+                properties:
+                  nodeAffinity:
+                    properties:
+                      preferredDuringSchedulingIgnoredDuringExecution:
+                        items:
+                          properties:
+                            preference:
+                              properties:
+                                matchExpressions:
+                                  items:
+                                    properties:
+                                      key:
+                                        type: string
+                                      operator:
+                                        type: string
+                                      values:
+                                        items:
+                                          type: string
+                                        type: array
+                                    type: object
+                                  type: array
+                                matchFields:
+                                  items:
+                                    properties:
+                                      key:
+                                        type: string
+                                      operator:
+                                        type: string
+                                      values:
+                                        items:
+                                          type: string
+                                        type: array
+                                    type: object
+                                  type: array
+                              type: object
+                            weight:
+                              type: integer
+                          type: object
+                        type: array
+                      requiredDuringSchedulingIgnoredDuringExecution:
+                        properties:
+                          nodeSelectorTerms:
+                            items:
+                              properties:
+                                matchExpressions:
+                                  items:
+                                    properties:
+                                      key:
+                                        type: string
+                                      operator:
+                                        type: string
+                                      values:
+                                        items:
+                                          type: string
+                                        type: array
+                                    type: object
+                                  type: array
+                                matchFields:
+                                  items:
+                                    properties:
+                                      key:
+                                        type: string
+                                      operator:
+                                        type: string
+                                      values:
+                                        items:
+                                          type: string
+                                        type: array
+                                    type: object
+                                  type: array
+                              type: object
+                            type: array
+                        type: object
+                    type: object
+                  podAffinity:
+                    properties:
+                      preferredDuringSchedulingIgnoredDuringExecution:
+                        items:
+                          properties:
+                            podAffinityTerm:
+                              properties:
+                                labelSelector:
+                                  properties:
+                                    matchExpressions:
+                                      items:
+                                        properties:
+                                          key:
+                                            type: string
+                                          operator:
+                                            type: string
+                                          values:
+                                            items:
+                                              type: string
+                                            type: array
+                                        type: object
+                                      type: array
+                                    matchLabels:
+                                      additionalProperties:
+                                        type: string
+                                      type: object
+                                  type: object
+                                namespaceSelector:
+                                  properties:
+                                    matchExpressions:
+                                      items:
+                                        properties:
+                                          key:
+                                            type: string
+                                          operator:
+                                            type: string
+                                          values:
+                                            items:
+                                              type: string
+                                            type: array
+                                        type: object
+                                      type: array
+                                    matchLabels:
+                                      additionalProperties:
+                                        type: string
+                                      type: object
+                                  type: object
+                                namespaces:
+                                  items:
+                                    type: string
+                                  type: array
+                                topologyKey:
+                                  type: string
+                              type: object
+                            weight:
+                              type: integer
+                          type: object
+                        type: array
+                      requiredDuringSchedulingIgnoredDuringExecution:
+                        items:
+                          properties:
+                            labelSelector:
+                              properties:
+                                matchExpressions:
+                                  items:
+                                    properties:
+                                      key:
+                                        type: string
+                                      operator:
+                                        type: string
+                                      values:
+                                        items:
+                                          type: string
+                                        type: array
+                                    type: object
+                                  type: array
+                                matchLabels:
+                                  additionalProperties:
+                                    type: string
+                                  type: object
+                              type: object
+                            namespaceSelector:
+                              properties:
+                                matchExpressions:
+                                  items:
+                                    properties:
+                                      key:
+                                        type: string
+                                      operator:
+                                        type: string
+                                      values:
+                                        items:
+                                          type: string
+                                        type: array
+                                    type: object
+                                  type: array
+                                matchLabels:
+                                  additionalProperties:
+                                    type: string
+                                  type: object
+                              type: object
+                            namespaces:
+                              items:
+                                type: string
+                              type: array
+                            topologyKey:
+                              type: string
+                          type: object
+                        type: array
+                    type: object
+                  podAntiAffinity:
+                    properties:
+                      preferredDuringSchedulingIgnoredDuringExecution:
+                        items:
+                          properties:
+                            podAffinityTerm:
+                              properties:
+                                labelSelector:
+                                  properties:
+                                    matchExpressions:
+                                      items:
+                                        properties:
+                                          key:
+                                            type: string
+                                          operator:
+                                            type: string
+                                          values:
+                                            items:
+                                              type: string
+                                            type: array
+                                        type: object
+                                      type: array
+                                    matchLabels:
+                                      additionalProperties:
+                                        type: string
+                                      type: object
+                                  type: object
+                                namespaceSelector:
+                                  properties:
+                                    matchExpressions:
+                                      items:
+                                        properties:
+                                          key:
+                                            type: string
+                                          operator:
+                                            type: string
+                                          values:
+                                            items:
+                                              type: string
+                                            type: array
+                                        type: object
+                                      type: array
+                                    matchLabels:
+                                      additionalProperties:
+                                        type: string
+                                      type: object
+                                  type: object
+                                namespaces:
+                                  items:
+                                    type: string
+                                  type: array
+                                topologyKey:
+                                  type: string
+                              type: object
+                            weight:
+                              type: integer
+                          type: object
+                        type: array
+                      requiredDuringSchedulingIgnoredDuringExecution:
+                        items:
+                          properties:
+                            labelSelector:
+                              properties:
+                                matchExpressions:
+                                  items:
+                                    properties:
+                                      key:
+                                        type: string
+                                      operator:
+                                        type: string
+                                      values:
+                                        items:
+                                          type: string
+                                        type: array
+                                    type: object
+                                  type: array
+                                matchLabels:
+                                  additionalProperties:
+                                    type: string
+                                  type: object
+                              type: object
+                            namespaceSelector:
+                              properties:
+                                matchExpressions:
+                                  items:
+                                    properties:
+                                      key:
+                                        type: string
+                                      operator:
+                                        type: string
+                                      values:
+                                        items:
+                                          type: string
+                                        type: array
+                                    type: object
+                                  type: array
+                                matchLabels:
+                                  additionalProperties:
+                                    type: string
+                                  type: object
+                              type: object
+                            namespaces:
+                              items:
+                                type: string
+                              type: array
+                            topologyKey:
+                              type: string
+                          type: object
+                        type: array
+                    type: object
+                type: object
+              topologySpreadConstraints:
+                items:
+                  properties:
+                    labelSelector:
+                      properties:
+                        matchExpressions:
+                          items:
+                            properties:
+                              key:
+                                type: string
+                              operator:
+                                type: string
+                              values:
+                                items:
+                                  type: string
+                                type: array
+                            type: object
+                          type: array
+                        matchLabels:
+                          additionalProperties:
+                            type: string
+                          type: object
+                      type: object
+                    matchLabelKeys:
+                      items:
+                        type: string
+                      type: array
+                    maxSkew:
+                      type: integer
+                    minDomains:
+                      type: integer
+                    nodeAffinityPolicy:
+                      type: string
+                    nodeTaintsPolicy:
+                      type: string
+                    topologyKey:
+                      type: string
+                    whenUnsatisfiable:
+                      type: string
+                  type: object
+                type: array
+              priorityClassName:
+                type: string
+              replicas:
+                type: integer
+              updatePartition:
+                type: integer
+              resources:
+                properties:
+                  claims:
+                    items:
+                      properties:
+                        name:
+                          type: string
+                      type: object
+                    type: array
+                  limits:
+                    additionalProperties:
+                      anyOf:
+                      - type: integer
+                      - type: string
+                      x-kubernetes-int-or-string: true
+                    type: object
+                  requests:
+                    additionalProperties:
+                      anyOf:
+                      - type: integer
+                      - type: string
+                      x-kubernetes-int-or-string: true
+                    type: object
+                type: object
+              startupProbe:
+                properties:
+                  exec:
+                    properties:
+                      command:
+                        items:
+                          type: string
+                        type: array
+                    type: object
+                  failureThreshold:
+                    type: integer
+                  grpc:
+                    properties:
+                      port:
+                        type: integer
+                      service:
+                        type: string
+                    type: object
+                  httpGet:
+                    properties:
+                      host:
+                        type: string
+                      httpHeaders:
+                        items:
+                          properties:
+                            name:
+                              type: string
+                            value:
+                              type: string
+                          type: object
+                        type: array
+                      path:
+                        type: string
+                      port:
+                        anyOf:
+                        - type: integer
+                        - type: string
+                        x-kubernetes-int-or-string: true
+                      scheme:
+                        type: string
+                    type: object
+                  initialDelaySeconds:
+                    type: integer
+                  periodSeconds:
+                    type: integer
+                  successThreshold:
+                    type: integer
+                  tcpSocket:
+                    properties:
+                      host:
+                        type: string
+                      port:
+                        anyOf:
+                        - type: integer
+                        - type: string
+                        x-kubernetes-int-or-string: true
+                    type: object
+                  terminationGracePeriodSeconds:
+                    type: integer
+                  timeoutSeconds:
+                    type: integer
+                type: object
+              readinessProbe:
+                properties:
+                  exec:
+                    properties:
+                      command:
+                        items:
+                          type: string
+                        type: array
+                    type: object
+                  failureThreshold:
+                    type: integer
+                  grpc:
+                    properties:
+                      port:
+                        type: integer
+                      service:
+                        type: string
+                    type: object
+                  httpGet:
+                    properties:
+                      host:
+                        type: string
+                      httpHeaders:
+                        items:
+                          properties:
+                            name:
+                              type: string
+                            value:
+                              type: string
+                          type: object
+                        type: array
+                      path:
+                        type: string
+                      port:
+                        anyOf:
+                        - type: integer
+                        - type: string
+                        x-kubernetes-int-or-string: true
+                      scheme:
+                        type: string
+                    type: object
+                  initialDelaySeconds:
+                    type: integer
+                  periodSeconds:
+                    type: integer
+                  successThreshold:
+                    type: integer
+                  tcpSocket:
+                    properties:
+                      host:
+                        type: string
+                      port:
+                        anyOf:
+                        - type: integer
+                        - type: string
+                        x-kubernetes-int-or-string: true
+                    type: object
+                  terminationGracePeriodSeconds:
+                    type: integer
+                  timeoutSeconds:
+                    type: integer
+                type: object
+              livenessProbe:
+                properties:
+                  exec:
+                    properties:
+                      command:
+                        items:
+                          type: string
+                        type: array
+                    type: object
+                  failureThreshold:
+                    type: integer
+                  grpc:
+                    properties:
+                      port:
+                        type: integer
+                      service:
+                        type: string
+                    type: object
+                  httpGet:
+                    properties:
+                      host:
+                        type: string
+                      httpHeaders:
+                        items:
+                          properties:
+                            name:
+                              type: string
+                            value:
+                              type: string
+                          type: object
+                        type: array
+                      path:
+                        type: string
+                      port:
+                        anyOf:
+                        - type: integer
+                        - type: string
+                        x-kubernetes-int-or-string: true
+                      scheme:
+                        type: string
+                    type: object
+                  initialDelaySeconds:
+                    type: integer
+                  periodSeconds:
+                    type: integer
+                  successThreshold:
+                    type: integer
+                  tcpSocket:
+                    properties:
+                      host:
+                        type: string
+                      port:
+                        anyOf:
+                        - type: integer
+                        - type: string
+                        x-kubernetes-int-or-string: true
+                    type: object
+                  terminationGracePeriodSeconds:
+                    type: integer
+                  timeoutSeconds:
+                    type: integer
+                type: object
+              gracefulStopTimeout:
+                type: integer
+              service:
+                properties:
+                  sshPort:
+                    type: integer
+                  type:
+                    type: string
+                  httpPort:
+                    type: integer
+                type: object
+              site:
+                properties:
+                  size:
+                    anyOf:
+                    - type: integer
+                    - type: string
+                    x-kubernetes-int-or-string: true
+                type: object
+              plugins:
+                items:
+                  properties:
+                    installAsLibrary:
+                      type: boolean
+                    name:
+                      type: string
+                    url:
+                      type: string
+                    sha1:
+                      type: string
+                  type: object
+                type: array
+              libs:
+                items:
+                  properties:
+                    name:
+                      type: string
+                    url:
+                      type: string
+                    sha1:
+                      type: string
+                  type: object
+                type: array
+              configFiles:
+                additionalProperties:
+                  type: string
+                type: object
+              secretRef:
+                type: string
+              mode:
+                enum:
+                - PRIMARY
+                - REPLICA
+                type: string
+              debug:
+                properties:
+                  enabled:
+                    type: boolean
+                  suspend:
+                    type: boolean
+                type: object
+            type: object
+          status:
+            properties:
+              ready:
+                type: boolean
+              appliedConfigMapVersions:
+                additionalProperties:
+                  type: string
+                type: object
+              appliedSecretVersions:
+                additionalProperties:
+                  type: string
+                type: object
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-operator-crds/templates/gitgcs.gerritoperator.google.com-v1.yml b/charts/k8s-gerrit/helm-charts/gerrit-operator-crds/templates/gitgcs.gerritoperator.google.com-v1.yml
new file mode 100644
index 0000000..7974e13
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-operator-crds/templates/gitgcs.gerritoperator.google.com-v1.yml
@@ -0,0 +1,386 @@
+# Generated by Fabric8 CRDGenerator, manual edits might get overwritten!
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  name: gitgcs.gerritoperator.google.com
+spec:
+  group: gerritoperator.google.com
+  names:
+    kind: GitGarbageCollection
+    plural: gitgcs
+    shortNames:
+    - gitgc
+    singular: gitgarbagecollection
+  scope: Namespaced
+  versions:
+  - name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        properties:
+          spec:
+            properties:
+              cluster:
+                type: string
+              schedule:
+                type: string
+              projects:
+                items:
+                  type: string
+                type: array
+              resources:
+                properties:
+                  claims:
+                    items:
+                      properties:
+                        name:
+                          type: string
+                      type: object
+                    type: array
+                  limits:
+                    additionalProperties:
+                      anyOf:
+                      - type: integer
+                      - type: string
+                      x-kubernetes-int-or-string: true
+                    type: object
+                  requests:
+                    additionalProperties:
+                      anyOf:
+                      - type: integer
+                      - type: string
+                      x-kubernetes-int-or-string: true
+                    type: object
+                type: object
+              tolerations:
+                items:
+                  properties:
+                    effect:
+                      type: string
+                    key:
+                      type: string
+                    operator:
+                      type: string
+                    tolerationSeconds:
+                      type: integer
+                    value:
+                      type: string
+                  type: object
+                type: array
+              affinity:
+                properties:
+                  nodeAffinity:
+                    properties:
+                      preferredDuringSchedulingIgnoredDuringExecution:
+                        items:
+                          properties:
+                            preference:
+                              properties:
+                                matchExpressions:
+                                  items:
+                                    properties:
+                                      key:
+                                        type: string
+                                      operator:
+                                        type: string
+                                      values:
+                                        items:
+                                          type: string
+                                        type: array
+                                    type: object
+                                  type: array
+                                matchFields:
+                                  items:
+                                    properties:
+                                      key:
+                                        type: string
+                                      operator:
+                                        type: string
+                                      values:
+                                        items:
+                                          type: string
+                                        type: array
+                                    type: object
+                                  type: array
+                              type: object
+                            weight:
+                              type: integer
+                          type: object
+                        type: array
+                      requiredDuringSchedulingIgnoredDuringExecution:
+                        properties:
+                          nodeSelectorTerms:
+                            items:
+                              properties:
+                                matchExpressions:
+                                  items:
+                                    properties:
+                                      key:
+                                        type: string
+                                      operator:
+                                        type: string
+                                      values:
+                                        items:
+                                          type: string
+                                        type: array
+                                    type: object
+                                  type: array
+                                matchFields:
+                                  items:
+                                    properties:
+                                      key:
+                                        type: string
+                                      operator:
+                                        type: string
+                                      values:
+                                        items:
+                                          type: string
+                                        type: array
+                                    type: object
+                                  type: array
+                              type: object
+                            type: array
+                        type: object
+                    type: object
+                  podAffinity:
+                    properties:
+                      preferredDuringSchedulingIgnoredDuringExecution:
+                        items:
+                          properties:
+                            podAffinityTerm:
+                              properties:
+                                labelSelector:
+                                  properties:
+                                    matchExpressions:
+                                      items:
+                                        properties:
+                                          key:
+                                            type: string
+                                          operator:
+                                            type: string
+                                          values:
+                                            items:
+                                              type: string
+                                            type: array
+                                        type: object
+                                      type: array
+                                    matchLabels:
+                                      additionalProperties:
+                                        type: string
+                                      type: object
+                                  type: object
+                                namespaceSelector:
+                                  properties:
+                                    matchExpressions:
+                                      items:
+                                        properties:
+                                          key:
+                                            type: string
+                                          operator:
+                                            type: string
+                                          values:
+                                            items:
+                                              type: string
+                                            type: array
+                                        type: object
+                                      type: array
+                                    matchLabels:
+                                      additionalProperties:
+                                        type: string
+                                      type: object
+                                  type: object
+                                namespaces:
+                                  items:
+                                    type: string
+                                  type: array
+                                topologyKey:
+                                  type: string
+                              type: object
+                            weight:
+                              type: integer
+                          type: object
+                        type: array
+                      requiredDuringSchedulingIgnoredDuringExecution:
+                        items:
+                          properties:
+                            labelSelector:
+                              properties:
+                                matchExpressions:
+                                  items:
+                                    properties:
+                                      key:
+                                        type: string
+                                      operator:
+                                        type: string
+                                      values:
+                                        items:
+                                          type: string
+                                        type: array
+                                    type: object
+                                  type: array
+                                matchLabels:
+                                  additionalProperties:
+                                    type: string
+                                  type: object
+                              type: object
+                            namespaceSelector:
+                              properties:
+                                matchExpressions:
+                                  items:
+                                    properties:
+                                      key:
+                                        type: string
+                                      operator:
+                                        type: string
+                                      values:
+                                        items:
+                                          type: string
+                                        type: array
+                                    type: object
+                                  type: array
+                                matchLabels:
+                                  additionalProperties:
+                                    type: string
+                                  type: object
+                              type: object
+                            namespaces:
+                              items:
+                                type: string
+                              type: array
+                            topologyKey:
+                              type: string
+                          type: object
+                        type: array
+                    type: object
+                  podAntiAffinity:
+                    properties:
+                      preferredDuringSchedulingIgnoredDuringExecution:
+                        items:
+                          properties:
+                            podAffinityTerm:
+                              properties:
+                                labelSelector:
+                                  properties:
+                                    matchExpressions:
+                                      items:
+                                        properties:
+                                          key:
+                                            type: string
+                                          operator:
+                                            type: string
+                                          values:
+                                            items:
+                                              type: string
+                                            type: array
+                                        type: object
+                                      type: array
+                                    matchLabels:
+                                      additionalProperties:
+                                        type: string
+                                      type: object
+                                  type: object
+                                namespaceSelector:
+                                  properties:
+                                    matchExpressions:
+                                      items:
+                                        properties:
+                                          key:
+                                            type: string
+                                          operator:
+                                            type: string
+                                          values:
+                                            items:
+                                              type: string
+                                            type: array
+                                        type: object
+                                      type: array
+                                    matchLabels:
+                                      additionalProperties:
+                                        type: string
+                                      type: object
+                                  type: object
+                                namespaces:
+                                  items:
+                                    type: string
+                                  type: array
+                                topologyKey:
+                                  type: string
+                              type: object
+                            weight:
+                              type: integer
+                          type: object
+                        type: array
+                      requiredDuringSchedulingIgnoredDuringExecution:
+                        items:
+                          properties:
+                            labelSelector:
+                              properties:
+                                matchExpressions:
+                                  items:
+                                    properties:
+                                      key:
+                                        type: string
+                                      operator:
+                                        type: string
+                                      values:
+                                        items:
+                                          type: string
+                                        type: array
+                                    type: object
+                                  type: array
+                                matchLabels:
+                                  additionalProperties:
+                                    type: string
+                                  type: object
+                              type: object
+                            namespaceSelector:
+                              properties:
+                                matchExpressions:
+                                  items:
+                                    properties:
+                                      key:
+                                        type: string
+                                      operator:
+                                        type: string
+                                      values:
+                                        items:
+                                          type: string
+                                        type: array
+                                    type: object
+                                  type: array
+                                matchLabels:
+                                  additionalProperties:
+                                    type: string
+                                  type: object
+                              type: object
+                            namespaces:
+                              items:
+                                type: string
+                              type: array
+                            topologyKey:
+                              type: string
+                          type: object
+                        type: array
+                    type: object
+                type: object
+            type: object
+          status:
+            properties:
+              replicateAll:
+                type: boolean
+              excludedProjects:
+                items:
+                  type: string
+                type: array
+              state:
+                enum:
+                - ACTIVE
+                - INACTIVE
+                - CONFLICT
+                - ERROR
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-operator-crds/templates/receivers.gerritoperator.google.com-v1.yml b/charts/k8s-gerrit/helm-charts/gerrit-operator-crds/templates/receivers.gerritoperator.google.com-v1.yml
new file mode 100644
index 0000000..c314670
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-operator-crds/templates/receivers.gerritoperator.google.com-v1.yml
@@ -0,0 +1,655 @@
+# Generated by Fabric8 CRDGenerator, manual edits might get overwritten!
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  name: receivers.gerritoperator.google.com
+spec:
+  group: gerritoperator.google.com
+  names:
+    kind: Receiver
+    plural: receivers
+    shortNames:
+    - grec
+    singular: receiver
+  scope: Namespaced
+  versions:
+  - name: v1alpha6
+    schema:
+      openAPIV3Schema:
+        properties:
+          spec:
+            properties:
+              containerImages:
+                properties:
+                  imagePullPolicy:
+                    type: string
+                  imagePullSecrets:
+                    items:
+                      properties:
+                        name:
+                          type: string
+                      type: object
+                    type: array
+                  busyBox:
+                    properties:
+                      registry:
+                        type: string
+                      tag:
+                        type: string
+                    type: object
+                  gerritImages:
+                    properties:
+                      registry:
+                        type: string
+                      org:
+                        type: string
+                      tag:
+                        type: string
+                    type: object
+                type: object
+              storage:
+                properties:
+                  storageClasses:
+                    properties:
+                      readWriteOnce:
+                        type: string
+                      readWriteMany:
+                        type: string
+                      nfsWorkaround:
+                        properties:
+                          enabled:
+                            type: boolean
+                          chownOnStartup:
+                            type: boolean
+                          idmapdConfig:
+                            type: string
+                        type: object
+                    type: object
+                  sharedStorage:
+                    properties:
+                      externalPVC:
+                        properties:
+                          enabled:
+                            type: boolean
+                          claimName:
+                            type: string
+                        type: object
+                      size:
+                        anyOf:
+                        - type: integer
+                        - type: string
+                        x-kubernetes-int-or-string: true
+                      volumeName:
+                        type: string
+                      selector:
+                        properties:
+                          matchExpressions:
+                            items:
+                              properties:
+                                key:
+                                  type: string
+                                operator:
+                                  type: string
+                                values:
+                                  items:
+                                    type: string
+                                  type: array
+                              type: object
+                            type: array
+                          matchLabels:
+                            additionalProperties:
+                              type: string
+                            type: object
+                        type: object
+                    type: object
+                type: object
+              ingress:
+                properties:
+                  enabled:
+                    type: boolean
+                  host:
+                    type: string
+                  tlsEnabled:
+                    type: boolean
+                  ssh:
+                    properties:
+                      enabled:
+                        type: boolean
+                    type: object
+                type: object
+              tolerations:
+                items:
+                  properties:
+                    effect:
+                      type: string
+                    key:
+                      type: string
+                    operator:
+                      type: string
+                    tolerationSeconds:
+                      type: integer
+                    value:
+                      type: string
+                  type: object
+                type: array
+              affinity:
+                properties:
+                  nodeAffinity:
+                    properties:
+                      preferredDuringSchedulingIgnoredDuringExecution:
+                        items:
+                          properties:
+                            preference:
+                              properties:
+                                matchExpressions:
+                                  items:
+                                    properties:
+                                      key:
+                                        type: string
+                                      operator:
+                                        type: string
+                                      values:
+                                        items:
+                                          type: string
+                                        type: array
+                                    type: object
+                                  type: array
+                                matchFields:
+                                  items:
+                                    properties:
+                                      key:
+                                        type: string
+                                      operator:
+                                        type: string
+                                      values:
+                                        items:
+                                          type: string
+                                        type: array
+                                    type: object
+                                  type: array
+                              type: object
+                            weight:
+                              type: integer
+                          type: object
+                        type: array
+                      requiredDuringSchedulingIgnoredDuringExecution:
+                        properties:
+                          nodeSelectorTerms:
+                            items:
+                              properties:
+                                matchExpressions:
+                                  items:
+                                    properties:
+                                      key:
+                                        type: string
+                                      operator:
+                                        type: string
+                                      values:
+                                        items:
+                                          type: string
+                                        type: array
+                                    type: object
+                                  type: array
+                                matchFields:
+                                  items:
+                                    properties:
+                                      key:
+                                        type: string
+                                      operator:
+                                        type: string
+                                      values:
+                                        items:
+                                          type: string
+                                        type: array
+                                    type: object
+                                  type: array
+                              type: object
+                            type: array
+                        type: object
+                    type: object
+                  podAffinity:
+                    properties:
+                      preferredDuringSchedulingIgnoredDuringExecution:
+                        items:
+                          properties:
+                            podAffinityTerm:
+                              properties:
+                                labelSelector:
+                                  properties:
+                                    matchExpressions:
+                                      items:
+                                        properties:
+                                          key:
+                                            type: string
+                                          operator:
+                                            type: string
+                                          values:
+                                            items:
+                                              type: string
+                                            type: array
+                                        type: object
+                                      type: array
+                                    matchLabels:
+                                      additionalProperties:
+                                        type: string
+                                      type: object
+                                  type: object
+                                namespaceSelector:
+                                  properties:
+                                    matchExpressions:
+                                      items:
+                                        properties:
+                                          key:
+                                            type: string
+                                          operator:
+                                            type: string
+                                          values:
+                                            items:
+                                              type: string
+                                            type: array
+                                        type: object
+                                      type: array
+                                    matchLabels:
+                                      additionalProperties:
+                                        type: string
+                                      type: object
+                                  type: object
+                                namespaces:
+                                  items:
+                                    type: string
+                                  type: array
+                                topologyKey:
+                                  type: string
+                              type: object
+                            weight:
+                              type: integer
+                          type: object
+                        type: array
+                      requiredDuringSchedulingIgnoredDuringExecution:
+                        items:
+                          properties:
+                            labelSelector:
+                              properties:
+                                matchExpressions:
+                                  items:
+                                    properties:
+                                      key:
+                                        type: string
+                                      operator:
+                                        type: string
+                                      values:
+                                        items:
+                                          type: string
+                                        type: array
+                                    type: object
+                                  type: array
+                                matchLabels:
+                                  additionalProperties:
+                                    type: string
+                                  type: object
+                              type: object
+                            namespaceSelector:
+                              properties:
+                                matchExpressions:
+                                  items:
+                                    properties:
+                                      key:
+                                        type: string
+                                      operator:
+                                        type: string
+                                      values:
+                                        items:
+                                          type: string
+                                        type: array
+                                    type: object
+                                  type: array
+                                matchLabels:
+                                  additionalProperties:
+                                    type: string
+                                  type: object
+                              type: object
+                            namespaces:
+                              items:
+                                type: string
+                              type: array
+                            topologyKey:
+                              type: string
+                          type: object
+                        type: array
+                    type: object
+                  podAntiAffinity:
+                    properties:
+                      preferredDuringSchedulingIgnoredDuringExecution:
+                        items:
+                          properties:
+                            podAffinityTerm:
+                              properties:
+                                labelSelector:
+                                  properties:
+                                    matchExpressions:
+                                      items:
+                                        properties:
+                                          key:
+                                            type: string
+                                          operator:
+                                            type: string
+                                          values:
+                                            items:
+                                              type: string
+                                            type: array
+                                        type: object
+                                      type: array
+                                    matchLabels:
+                                      additionalProperties:
+                                        type: string
+                                      type: object
+                                  type: object
+                                namespaceSelector:
+                                  properties:
+                                    matchExpressions:
+                                      items:
+                                        properties:
+                                          key:
+                                            type: string
+                                          operator:
+                                            type: string
+                                          values:
+                                            items:
+                                              type: string
+                                            type: array
+                                        type: object
+                                      type: array
+                                    matchLabels:
+                                      additionalProperties:
+                                        type: string
+                                      type: object
+                                  type: object
+                                namespaces:
+                                  items:
+                                    type: string
+                                  type: array
+                                topologyKey:
+                                  type: string
+                              type: object
+                            weight:
+                              type: integer
+                          type: object
+                        type: array
+                      requiredDuringSchedulingIgnoredDuringExecution:
+                        items:
+                          properties:
+                            labelSelector:
+                              properties:
+                                matchExpressions:
+                                  items:
+                                    properties:
+                                      key:
+                                        type: string
+                                      operator:
+                                        type: string
+                                      values:
+                                        items:
+                                          type: string
+                                        type: array
+                                    type: object
+                                  type: array
+                                matchLabels:
+                                  additionalProperties:
+                                    type: string
+                                  type: object
+                              type: object
+                            namespaceSelector:
+                              properties:
+                                matchExpressions:
+                                  items:
+                                    properties:
+                                      key:
+                                        type: string
+                                      operator:
+                                        type: string
+                                      values:
+                                        items:
+                                          type: string
+                                        type: array
+                                    type: object
+                                  type: array
+                                matchLabels:
+                                  additionalProperties:
+                                    type: string
+                                  type: object
+                              type: object
+                            namespaces:
+                              items:
+                                type: string
+                              type: array
+                            topologyKey:
+                              type: string
+                          type: object
+                        type: array
+                    type: object
+                type: object
+              topologySpreadConstraints:
+                items:
+                  properties:
+                    labelSelector:
+                      properties:
+                        matchExpressions:
+                          items:
+                            properties:
+                              key:
+                                type: string
+                              operator:
+                                type: string
+                              values:
+                                items:
+                                  type: string
+                                type: array
+                            type: object
+                          type: array
+                        matchLabels:
+                          additionalProperties:
+                            type: string
+                          type: object
+                      type: object
+                    matchLabelKeys:
+                      items:
+                        type: string
+                      type: array
+                    maxSkew:
+                      type: integer
+                    minDomains:
+                      type: integer
+                    nodeAffinityPolicy:
+                      type: string
+                    nodeTaintsPolicy:
+                      type: string
+                    topologyKey:
+                      type: string
+                    whenUnsatisfiable:
+                      type: string
+                  type: object
+                type: array
+              priorityClassName:
+                type: string
+              replicas:
+                type: integer
+              maxSurge:
+                anyOf:
+                - type: integer
+                - type: string
+                x-kubernetes-int-or-string: true
+              maxUnavailable:
+                anyOf:
+                - type: integer
+                - type: string
+                x-kubernetes-int-or-string: true
+              resources:
+                properties:
+                  claims:
+                    items:
+                      properties:
+                        name:
+                          type: string
+                      type: object
+                    type: array
+                  limits:
+                    additionalProperties:
+                      anyOf:
+                      - type: integer
+                      - type: string
+                      x-kubernetes-int-or-string: true
+                    type: object
+                  requests:
+                    additionalProperties:
+                      anyOf:
+                      - type: integer
+                      - type: string
+                      x-kubernetes-int-or-string: true
+                    type: object
+                type: object
+              readinessProbe:
+                properties:
+                  exec:
+                    properties:
+                      command:
+                        items:
+                          type: string
+                        type: array
+                    type: object
+                  failureThreshold:
+                    type: integer
+                  grpc:
+                    properties:
+                      port:
+                        type: integer
+                      service:
+                        type: string
+                    type: object
+                  httpGet:
+                    properties:
+                      host:
+                        type: string
+                      httpHeaders:
+                        items:
+                          properties:
+                            name:
+                              type: string
+                            value:
+                              type: string
+                          type: object
+                        type: array
+                      path:
+                        type: string
+                      port:
+                        anyOf:
+                        - type: integer
+                        - type: string
+                        x-kubernetes-int-or-string: true
+                      scheme:
+                        type: string
+                    type: object
+                  initialDelaySeconds:
+                    type: integer
+                  periodSeconds:
+                    type: integer
+                  successThreshold:
+                    type: integer
+                  tcpSocket:
+                    properties:
+                      host:
+                        type: string
+                      port:
+                        anyOf:
+                        - type: integer
+                        - type: string
+                        x-kubernetes-int-or-string: true
+                    type: object
+                  terminationGracePeriodSeconds:
+                    type: integer
+                  timeoutSeconds:
+                    type: integer
+                type: object
+              livenessProbe:
+                properties:
+                  exec:
+                    properties:
+                      command:
+                        items:
+                          type: string
+                        type: array
+                    type: object
+                  failureThreshold:
+                    type: integer
+                  grpc:
+                    properties:
+                      port:
+                        type: integer
+                      service:
+                        type: string
+                    type: object
+                  httpGet:
+                    properties:
+                      host:
+                        type: string
+                      httpHeaders:
+                        items:
+                          properties:
+                            name:
+                              type: string
+                            value:
+                              type: string
+                          type: object
+                        type: array
+                      path:
+                        type: string
+                      port:
+                        anyOf:
+                        - type: integer
+                        - type: string
+                        x-kubernetes-int-or-string: true
+                      scheme:
+                        type: string
+                    type: object
+                  initialDelaySeconds:
+                    type: integer
+                  periodSeconds:
+                    type: integer
+                  successThreshold:
+                    type: integer
+                  tcpSocket:
+                    properties:
+                      host:
+                        type: string
+                      port:
+                        anyOf:
+                        - type: integer
+                        - type: string
+                        x-kubernetes-int-or-string: true
+                    type: object
+                  terminationGracePeriodSeconds:
+                    type: integer
+                  timeoutSeconds:
+                    type: integer
+                type: object
+              service:
+                properties:
+                  type:
+                    type: string
+                  httpPort:
+                    type: integer
+                type: object
+              credentialSecretRef:
+                type: string
+            type: object
+          status:
+            properties:
+              ready:
+                type: boolean
+              appliedCredentialSecretVersion:
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-operator-crds/values.yaml b/charts/k8s-gerrit/helm-charts/gerrit-operator-crds/values.yaml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-operator-crds/values.yaml
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-operator/.helmignore b/charts/k8s-gerrit/helm-charts/gerrit-operator/.helmignore
new file mode 100644
index 0000000..0e8a0eb
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-operator/.helmignore
@@ -0,0 +1,23 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*.orig
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
+.vscode/
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-operator/Chart.yaml b/charts/k8s-gerrit/helm-charts/gerrit-operator/Chart.yaml
new file mode 100644
index 0000000..21ce467
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-operator/Chart.yaml
@@ -0,0 +1,12 @@
+apiVersion: v2
+name: gerrit-operator
+description: |
+  This helm chart creates a Deployment for the gerrit-operator. A corresponding
+  Service for the operator is implicitly created.
+sources:
+- https://gerrit.googlesource.com/k8s-gerrit/+/refs/heads/master/operator
+version : 0.1.0
+dependencies:
+- name: gerrit-operator-crds
+  version: 0.1.0
+  repository: "file://../gerrit-operator-crds"
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-operator/templates/operator.yaml b/charts/k8s-gerrit/helm-charts/gerrit-operator/templates/operator.yaml
new file mode 100644
index 0000000..f9ed84f
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-operator/templates/operator.yaml
@@ -0,0 +1,71 @@
+{{- if .Values.externalKeyStore.enabled }}
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name:  gerrit-operator-ssl
+  namespace: {{ .Release.Namespace }}
+data:
+  keystore.jks: {{ .Values.externalKeyStore.jks }}
+  keystore.password: {{ .Values.externalKeyStore.password | b64enc }}
+type: Opaque
+{{- end }}
+
+
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: gerrit-operator
+  namespace: {{ .Release.Namespace }}
+spec:
+  selector:
+    matchLabels:
+      app: gerrit-operator
+  template:
+    metadata:
+      labels:
+        app: gerrit-operator
+    spec:
+      serviceAccountName: gerrit-operator
+      {{- with .Values.image.imagePullSecrets }}
+      imagePullSecrets:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      containers:
+      - name: operator
+        image: {{ .Values.image.registry | default "docker.io" }}/{{ .Values.image.org }}/{{ .Values.image.name }}:{{ .Values.image.tag | default "latest" }}
+        imagePullPolicy: {{ .Values.image.imagePullPolicy }}
+        env:
+        - name: NAMESPACE
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.namespace
+        - name: INGRESS
+          value: {{ .Values.ingress.type }}
+        ports:
+        - containerPort: 80
+        readinessProbe:
+          httpGet:
+            path: /health
+            port: 8080
+            scheme: HTTPS
+          initialDelaySeconds: 10
+        livenessProbe:
+          httpGet:
+            path: /health
+            port: 8080
+            scheme: HTTPS
+          initialDelaySeconds: 30
+        {{- if .Values.externalKeyStore.enabled }}
+        volumeMounts:
+        - name: ssl
+          readOnly: true
+          mountPath: /operator
+        {{- end }}
+      {{- if .Values.externalKeyStore.enabled }}
+      volumes:
+      - name: ssl
+        secret:
+          secretName: gerrit-operator-ssl
+      {{- end }}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-operator/templates/rbac.yaml b/charts/k8s-gerrit/helm-charts/gerrit-operator/templates/rbac.yaml
new file mode 100644
index 0000000..fbd2ae7
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-operator/templates/rbac.yaml
@@ -0,0 +1,87 @@
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: gerrit-operator
+  namespace: {{ .Release.Namespace }}
+
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  name: gerrit-operator-admin
+subjects:
+- kind: ServiceAccount
+  name: gerrit-operator
+  namespace: {{ .Release.Namespace }}
+roleRef:
+  kind: ClusterRole
+  name: gerrit-operator
+  apiGroup: ""
+
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: gerrit-operator
+rules:
+- apiGroups:
+  - "batch"
+  resources:
+  - cronjobs
+  verbs:
+  - '*'
+- apiGroups:
+  - "apps"
+  resources:
+  - statefulsets
+  - deployments
+  verbs:
+  - '*'
+- apiGroups:
+  - ""
+  resources:
+  - configmaps
+  - persistentvolumeclaims
+  - secrets
+  - services
+  verbs:
+  - '*'
+- apiGroups:
+  - "storage.k8s.io"
+  resources:
+  - storageclasses
+  verbs:
+  - 'get'
+  - 'list'
+- apiGroups:
+  - "apiextensions.k8s.io"
+  resources:
+  - customresourcedefinitions
+  verbs:
+  - '*'
+- apiGroups:
+  - "networking.k8s.io"
+  resources:
+  - ingresses
+  verbs:
+  - '*'
+- apiGroups:
+  - "gerritoperator.google.com"
+  resources:
+  - '*'
+  verbs:
+  - '*'
+- apiGroups:
+  - "networking.istio.io"
+  resources:
+  - "gateways"
+  - "virtualservices"
+  - "destinationrules"
+  verbs:
+  - '*'
+- apiGroups:
+  - "admissionregistration.k8s.io"
+  resources:
+  - 'validatingwebhookconfigurations'
+  verbs:
+  - '*'
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-operator/values.yaml b/charts/k8s-gerrit/helm-charts/gerrit-operator/values.yaml
new file mode 100644
index 0000000..ebb88af
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-operator/values.yaml
@@ -0,0 +1,21 @@
+image:
+  registry: docker.io
+  org: k8sgerrit
+  name: gerrit-operator
+  tag: latest
+  imagePullPolicy: Always
+  imagePullSecrets: []
+  # - name: my-secret-1
+
+ingress:
+  # Which ingress provider to use (options: NONE, INGRESS, ISTIO)
+  type: NONE
+
+## Required to use an external/persistent keystore, otherwise a keystore using
+## self-signed certificates will be generated
+externalKeyStore:
+  enabled: false
+  # base64-encoded Java keystore
+  jks: ""
+  # Java keystore password (not base64-encoded)
+  password: ""
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-replica/.helmignore b/charts/k8s-gerrit/helm-charts/gerrit-replica/.helmignore
new file mode 100644
index 0000000..4f4562f
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-replica/.helmignore
@@ -0,0 +1,24 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
+
+docs/
+supplements/
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-replica/Chart.yaml b/charts/k8s-gerrit/helm-charts/gerrit-replica/Chart.yaml
new file mode 100644
index 0000000..1f54472
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-replica/Chart.yaml
@@ -0,0 +1,24 @@
+apiVersion: v2
+appVersion: 3.8.0
+description: |-
+    The Gerrit replica serves as a read-only Gerrit instance to serve repositories
+    that it receives from a Gerrit instance via replication. It can be used to
+    reduce the load on Gerrit instances.
+name: gerrit-replica
+version: 0.2.0
+maintainers:
+- name: Thomas Draebing
+  email: thomas.draebing@sap.com
+- name: Matthias Sohn
+  email: matthias.sohn@sap.com
+- name: Sasa Zivkov
+  email: sasa.zivkov@sap.com
+- name: Christian Halstrick
+  email: christian.halstrick@sap.com
+home: https://gerrit.googlesource.com/k8s-gerrit/+/master/helm-charts/gerrit-replica
+icon: http://commondatastorage.googleapis.com/gerrit-static/diffy-w200.png
+sources:
+- https://gerrit.googlesource.com/k8s-gerrit/+/master
+keywords:
+- gerrit
+- git
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-replica/LICENSE b/charts/k8s-gerrit/helm-charts/gerrit-replica/LICENSE
new file mode 100644
index 0000000..028fc9f
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-replica/LICENSE
@@ -0,0 +1,201 @@
+   Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "{}"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright (C) 2018 The Android Open Source Project
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-replica/README.md b/charts/k8s-gerrit/helm-charts/gerrit-replica/README.md
new file mode 100644
index 0000000..993f4d9
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-replica/README.md
@@ -0,0 +1,546 @@
+# Gerrit replica on Kubernetes
+
+Gerrit is a web-based code review tool, which acts as a Git server. On large setups
+Gerrit servers can see a sizable amount of traffic from git operations performed by
+developers and build servers. The major part of requests are read-only requests
+(e.g. by `git fetch` operations). To take some load of the Gerrit server,
+Gerrit replicas can be deployed to serve read-only requests.
+
+This helm chart provides a Gerrit replica setup that can be deployed on Kubernetes.
+The Gerrit replica is capable of receiving replicated git repositories from a
+Gerrit. The Gerrit replica can then serve authenticated read-only requests.
+
+***note
+Gerrit versions before 3.0 are no longer supported, since the support of ReviewDB
+was removed.
+***
+
+## Prerequisites
+
+- Helm (>= version 3.0)
+
+    (Check out [this guide](https://docs.helm.sh/using_helm/#quickstart-guide)
+    how to install and use helm.)
+
+- Access to a provisioner for persistent volumes with `Read Write Many (RWM)`-
+  capability.
+
+    A list of applicaple volume types can be found
+    [here](https://kubernetes.io/docs/concepts/storage/persistent-volumes/#access-modes).
+    This project was developed using the
+    [NFS-server-provisioner helm chart](https://github.com/helm/charts/tree/master/stable/nfs-server-provisioner),
+    a NFS-provisioner deployed in the Kubernetes cluster itself. Refer to
+    [this guide](/helm-charts/gerrit-replica/docs/nfs-provisioner.md) of how to
+    deploy it in context of this project.
+
+- A domain name that is configured to point to the IP address of the node running
+  the Ingress controller on the kubernetes cluster (as described
+  [here](http://alesnosek.com/blog/2017/02/14/accessing-kubernetes-pods-from-outside-of-the-cluster/)).
+
+- (Optional: Required, if SSL is configured)
+  A [Java keystore](https://gerrit-review.googlesource.com/Documentation/config-gerrit.html#httpd.sslKeyStore)
+  to be used by Gerrit.
+
+## Installing the Chart
+
+***note
+**ATTENTION:** The value for `ingress.host` is required for rendering
+the chart's templates. The nature of the value does not allow defaults.
+Thus a custom `values.yaml`-file setting this value is required!
+***
+
+To install the chart with the release name `gerrit-replica`, execute:
+
+```sh
+cd $(git rev-parse --show-toplevel)/helm-charts
+helm install \
+  gerrit-replica \  # release name
+  ./gerrit-replica \  # path to chart
+  -f <path-to-custom-values>.yaml
+```
+
+The command deploys the Gerrit replica on the current Kubernetes cluster. The
+[configuration section](#Configuration) lists the parameters that can be
+configured during installation.
+
+The Gerrit replica requires the replicated `All-Projects.git`- and `All-Users.git`-
+repositories to be present in the `/var/gerrit/git`-directory. The `gerrit-init`-
+InitContainer will wait for this being the case. A way to do this is to access
+the Gerrit replica pod and to clone the repositories from the primary Gerrit (Make
+sure that you have the correct access rights do so.):
+
+```sh
+kubectl exec -it <gerrit-replica-pod> -c gerrit-init ash
+gerrit@<gerrit-replica-pod>:/var/tools$ cd /var/gerrit/git
+gerrit@<gerrit-replica-pod>:/var/gerrit/git$ git clone "http://gerrit.com/All-Projects" --mirror
+Cloning into bare repository 'All-Projects.git'...
+gerrit@<gerrit-replica-pod>:/var/gerrit/git$ git clone "http://gerrit.com/All-Users" --mirror
+Cloning into bare repository 'All-Users.git'...
+```
+
+## Configuration
+
+The following sections list the configurable values in `values.yaml`. To configure
+a Gerrit replica setup, make a copy of the `values.yaml`-file and change the
+parameters as needed. The configuration can be applied by installing the chart as
+described [above](#Installing-the-chart).
+
+In addition, single options can be set without creating a custom `values.yaml`:
+
+```sh
+cd $(git rev-parse --show-toplevel)/helm-charts
+helm install \
+  gerrit-replica \  # release name
+  ./gerrit-replica \  # path to chart
+  --set=gitRepositoryStorage.size=100Gi,gitBackend.replicas=2
+```
+
+### Container images
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `images.busybox.registry` | The registry to pull the busybox container images from | `docker.io` |
+| `images.busybox.tag` | The busybox image tag to use | `latest` |
+| `images.registry.name` | The image registry to pull the container images from | `` |
+| `images.registry.ImagePullSecret.name` | Name of the ImagePullSecret | `image-pull-secret` (if empty no image pull secret will be deployed) |
+| `images.registry.ImagePullSecret.create` | Whether to create an ImagePullSecret | `false` |
+| `images.registry.ImagePullSecret.username` | The image registry username | `nil` |
+| `images.registry.ImagePullSecret.password` | The image registry password | `nil` |
+| `images.version` | The image version (image tag) to use | `latest` |
+| `images.imagePullPolicy` | Image pull policy | `Always` |
+| `images.additionalImagePullSecrets` | Additional image pull policies that pods should use | `[]` |
+
+### Labels
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `additionalLabels` | Additional labels for resources managed by this Helm chart | `{}` |
+
+### Storage classes
+
+For information of how a `StorageClass` is configured in Kubernetes, read the
+[official Documentation](https://kubernetes.io/docs/concepts/storage/storage-classes/#introduction).
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `storageClasses.default.name` | The name of the default StorageClass (RWO) | `default` |
+| `storageClasses.default.create` | Whether to create the StorageClass | `false` |
+| `storageClasses.default.provisioner` | Provisioner of the StorageClass | `kubernetes.io/aws-ebs` |
+| `storageClasses.default.reclaimPolicy` | Whether to `Retain` or `Delete` volumes, when they become unbound | `Delete` |
+| `storageClasses.default.parameters` | Parameters for the provisioner | `parameters.type: gp2`, `parameters.fsType: ext4` |
+| `storageClasses.default.mountOptions` | The mount options of the default StorageClass | `[]` |
+| `storageClasses.default.allowVolumeExpansion` | Whether to allow volume expansion. | `false` |
+| `storageClasses.shared.name` | The name of the shared StorageClass (RWM) | `shared-storage` |
+| `storageClasses.shared.create` | Whether to create the StorageClass | `false` |
+| `storageClasses.shared.provisioner` | Provisioner of the StorageClass | `nfs` |
+| `storageClasses.shared.reclaimPolicy` | Whether to `Retain` or `Delete` volumes, when they become unbound | `Delete` |
+| `storageClasses.shared.parameters` | Parameters for the provisioner | `parameters.mountOptions: vers=4.1` |
+| `storageClasses.shared.mountOptions` | The mount options of the shared StorageClass | `[]` |
+| `storageClasses.shared.allowVolumeExpansion` | Whether to allow volume expansion. | `false` |
+
+### CA certificate
+
+Some application may require TLS verification. If the default CA built into the
+containers is not enough a custom CA certificate can be given to the deployment.
+Note, that Gerrit will require its CA in a JKS keytore, which is described below.
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `caCert` | CA certificate for TLS verification (if not set, the default will be used) | `None` |
+
+### Workaround for NFS
+
+Kubernetes will not always be able to adapt the ownership of the files within NFS
+volumes. Thus, a workaround exists that will add init-containers to
+adapt file ownership. Note, that only the ownership of the root directory of the
+volume will be changed. All data contained within will be expected to already be
+owned by the user used by Gerrit. Also the ID-domain will be configured to ensure
+correct ID-mapping.
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `nfsWorkaround.enabled` | Whether the volume used is an NFS-volume | `false` |
+| `nfsWorkaround.chownOnStartup` | Whether to chown the volume on pod startup | `false` |
+| `nfsWorkaround.idDomain` | The ID-domain that should be used to map user-/group-IDs for the NFS mount | `localdomain.com` |
+
+### Network policies
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `networkPolicies.enabled` | Whether to enable preconfigured NetworkPolicies | `false` |
+| `networkPolicies.dnsPorts` | List of ports used by DNS-service (e.g. KubeDNS) | `[53, 8053]` |
+
+The NetworkPolicies provided here are quite strict and do not account for all
+possible scenarios. Thus, custom NetworkPolicies have to be added, e.g. for
+connecting to a database. On the other hand some defaults may be not restrictive
+enough. By default, the ingress traffic of the git-backend pod is not restricted.
+Thus, every source (with the right credentials) could push to the git-backend.
+To add an additional layer of security, the ingress rule could be defined more
+finegrained. The chart provides the possibility to define custom rules for ingress-
+traffic of the git-backend pod under `gitBackend.networkPolicy.ingress`.
+Depending on the scenario, there are different ways to restrict the incoming
+connections.
+
+If the replicator (e.g. Gerrit) is running in a pod on the same cluster,
+a podSelector (and namespaceSelector, if the pod is running in a different
+namespace) can be used to whitelist the traffic:
+
+```yaml
+gitBackend:
+  networkPolicy:
+    ingress:
+    - from:
+      - podSelector:
+          matchLabels:
+            app: gerrit
+```
+
+If the replicator is outside the cluster, the IP of the replicator can also be
+whitelisted, e.g.:
+
+```yaml
+gitBackend:
+  networkPolicy:
+    ingress:
+    - from:
+      - ipBlock:
+          cidr: xxx.xxx.0.0/16
+```
+
+The same principle also applies to other use cases, e.g. connecting to a database.
+For more information about the NetworkPolicy resource refer to the
+[Kubernetes documentation](https://kubernetes.io/docs/concepts/services-networking/network-policies/).
+
+### Storage for Git repositories
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `gitRepositoryStorage.externalPVC.use` | Whether to use a PVC deployed outside the chart | `false` |
+| `gitRepositoryStorage.externalPVC.name` | Name of the external PVC | `git-repositories-pvc` |
+| `gitRepositoryStorage.size` | Size of the volume storing the Git repositories | `5Gi` |
+
+If the git repositories should be persisted even if the chart is deleted and in
+a way that the volume containing them can be mounted by the reinstalled chart,
+the PVC claiming the volume has to be created independently of the chart. To use
+the external PVC, set `gitRepositoryStorage.externalPVC.enabled` to `true` and
+give the name of the PVC under `gitRepositoryStorage.externalPVC.name`.
+
+### Storage for Logs
+
+In addition to collecting logs with a log collection tool like Promtail, the logs
+can also be stored in a persistent volume. This volume has to be a read-write-many
+volume to be able to be used by multiple pods.
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `logStorage.enabled` | Whether to enable persistence of logs | `false` |
+| `logStorage.externalPVC.use` | Whether to use a PVC deployed outside the chart | `false` |
+| `logStorage.externalPVC.name` | Name of the external PVC | `gerrit-logs-pvc` |
+| `logStorage.size` | Size of the volume | `5Gi` |
+| `logStorage.cleanup.enabled` | Whether to regularly delete old logs | `false` |
+| `logStorage.cleanup.schedule` | Cron schedule defining when to run the cleanup job | `0 0 * * *` |
+| `logStorage.cleanup.retentionDays` | Number of days to retain the logs | `14` |
+| `logStorage.cleanup.resources` | Resources the container is allowed to use | `requests.cpu: 100m` |
+| `logStorage.cleanup.additionalPodLabels` | Additional labels for pods | `{}` |
+| | | `requests.memory: 256Mi` |
+| | | `limits.cpu: 100m` |
+| | | `limits.memory: 256Mi` |
+
+Each pod will create a separate folder for its logs, allowing to trace logs to
+the respective pods.
+
+### Istio
+
+Istio can be used as an alternative to Kubernetes Ingresses to manage the traffic
+into the cluster and also inside the cluster. This requires istio to be installed
+beforehand. Some guidance on how to set up istio can be found [here](/Documentation/istio.md).
+The helm chart expects `istio-injection` to be enabled in the namespace, in which
+it will be installed.
+
+In the case istio is used, all configuration for ingresses in the chart will be
+ignored.
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `istio.enabled` | Whether istio should be used (requires istio to be installed) | `false` |
+| `istio.host` | Hostname (CNAME must point to istio ingress gateway loadbalancer service) | `nil` |
+| `istio.tls.enabled` | Whether to enable TLS | `false` |
+| `istio.tls.secret.create` | Whether to create TLS certificate secret | `true` |
+| `istio.tls.secret.name` | Name of external secret containing TLS certificates | `nil` |
+| `istio.tls.cert` | TLS certificate | `-----BEGIN CERTIFICATE-----` |
+| `istio.tls.key` | TLS key | `-----BEGIN RSA PRIVATE KEY-----` |
+| `istio.ssh.enabled` | Whether to enable SSH | `false` |
+
+### Ingress
+
+As an alternative to istio the Nginx Ingress controller can be used to manage
+ingress traffic.
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `ingress.enabled` | Whether to deploy an Ingress | `false` |
+| `ingress.host` | Host name to use for the Ingress (required for Ingress) | `nil` |
+| `ingress.maxBodySize` | Maximum request body size allowed (Set to 0 for an unlimited request body size) | `50m` |
+| `ingress.additionalAnnotations` | Additional annotations for the Ingress | `nil` |
+| `ingress.tls.enabled` | Whether to enable TLS termination in the Ingress | `false` |
+| `ingress.tls.secret.create` | Whether to create a TLS-secret | `true` |
+| `ingress.tls.secret.name` | Name of an external secret that will be used as a TLS-secret | `nil` |
+| `ingress.tls.cert` | Public SSL server certificate | `-----BEGIN CERTIFICATE-----` |
+| `ingress.tls.key` | Private SSL server certificate | `-----BEGIN RSA PRIVATE KEY-----` |
+
+***note
+For graceful shutdown to work with an ingress, the ingress controller has to be
+configured to gracefully close the connections as well.
+***
+
+### Promtail Sidecar
+
+To collect Gerrit logs, a Promtail sidecar can be deployed into the Gerrit replica
+pods. This can for example be used together with the [gerrit-monitoring](https://gerrit-review.googlesource.com/admin/repos/gerrit-monitoring)
+project.
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `promtailSidecar.enabled` | Whether to install the Promatil sidecar container | `false` |
+| `promtailSidecar.image` | The promtail container image to use | `grafana/promtail` |
+| `promtailSidecar.version` | The promtail container image version | `1.3.0` |
+| `promtailSidecar.resources` | Configure the amount of resources the container requests/is allowed | `requests.cpu: 100m` |
+|                             |                                                                     | `requests.memory: 128Mi` |
+|                             |                                                                     | `limits.cpu: 200m` |
+|                             |                                                                     | `limits.memory: 128Mi` |
+| `promtailSidecar.tls.skipverify` | Whether to skip TLS verification | `true` |
+| `promtailSidecar.tls.caCert` | CA certificate for TLS verification | `-----BEGIN CERTIFICATE-----` |
+| `promtailSidecar.loki.url` | URL to reach Loki | `loki.example.com` |
+| `promtailSidecar.loki.user` | Loki user | `admin` |
+| `promtailSidecar.loki.password` | Loki password | `secret` |
+
+
+### Apache-Git-HTTP-Backend (Git-Backend)
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `gitBackend.image` | Image name of the Apache-git-http-backend container image | `k8sgerrit/apache-git-http-backend` |
+| `gitBackend.additionalPodLabels` | Additional labels for Pods | `{}` |
+| `gitBackend.tolerations` | Taints and tolerations work together to ensure that pods are not scheduled onto inappropriate nodes. For more information, please refer to the following documents. [Taints and Tolerations](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration) | [] |
+| `gitBackend.topologySpreadConstraints` | Control how Pods are spread across your cluster among failure-domains. For more information, please refer to the following documents. [Pod Topology Spread Constraints](https://kubernetes.io/docs/concepts/workloads/pods/pod-topology-spread-constraints) | {} |
+| `gitBackend.nodeSelector` | Assigns a Pod to the specified Nodes. For more information, please refer to the following documents. [Assign Pods to Nodes](https://kubernetes.io/docs/tasks/configure-pod-container/assign-pods-nodes/). [Assigning Pods to Nodes](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/) | {} |
+| `gitBackend.affinity` | Assigns a Pod to the specified Nodes | podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].weight: 100 |
+|                       |                                      | podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].podAffinityTerm.topologyKey: "topology.kubernetes.io/zone" |
+|                       |                                      | podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].podAffinityTerm.labelSelector.matchExpressions[0].key: app |
+|                       |                                      | podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].podAffinityTerm.labelSelector.matchExpressions[0].operator: In |
+|                       |                                      | podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].podAffinityTerm.labelSelector.matchExpressions[0].values[0]: git-backend |
+| `gitBackend.replicas` | Number of pod replicas to deploy | `1` |
+| `gitBackend.maxSurge` | Max. percentage or number of pods allowed to be scheduled above the desired number | `25%` |
+| `gitBackend.maxUnavailable` | Max. percentage or number of pods allowed to be unavailable at a time | `100%` |
+| `gitBackend.networkPolicy.ingress` | Custom ingress-network policy for git-backend pods | `[{}]` (allow all) |
+| `gitBackend.networkPolicy.egress` | Custom egress-network policy for git-backend pods | `nil` |
+| `gitBackend.resources` | Configure the amount of resources the pod requests/is allowed | `requests.cpu: 100m` |
+|                        |                                                               | `requests.memory: 256Mi` |
+|                        |                                                               | `limits.cpu: 100m` |
+|                        |                                                               | `limits.memory: 256Mi` |
+| `gitBackend.livenessProbe` | Configuration of the liveness probe timings | `{initialDelaySeconds: 10, periodSeconds: 5}` |
+| `gitBackend.readinessProbe` | Configuration of the readiness probe timings | `{initialDelaySeconds: 5, periodSeconds: 1}` |
+| `gitBackend.credentials.htpasswd` | `.htpasswd`-file containing username/password-credentials for accessing git | `git:$apr1$O/LbLKC7$Q60GWE7OcqSEMSfe/K8xU.` (user: git, password: secret) |
+| `gitBackend.service.additionalAnnotations` | Additional annotations for the Service | `{}` |
+| `gitBackend.service.loadBalancerSourceRanges` | The list of allowed IPs for the Service | `[]` |
+| `gitBackend.service.type` | Which kind of Service to deploy | `LoadBalancer` |
+| `gitBackend.service.externalTrafficPolicy` | Specify how traffic from external is handled | `Cluster` |
+| `gitBackend.service.http.enabled` | Whether to serve HTTP-requests (needed for Ingress) | `true` |
+| `gitBackend.service.http.port` | Port over which to expose HTTP | `80` |
+| `gitBackend.service.https.enabled` | Whether to serve HTTPS-requests | `false` |
+| `gitBackend.service.https.port` | Port over which to expose HTTPS | `443` |
+
+***note
+At least one endpoint (HTTP and/or HTTPS) has to be enabled in the service!
+***
+
+Project creation, project deletion and HEAD update can also replicated. To enable
+this feature configure the replication plugin to use an adminUrl using the format
+`gerrit+https://<apache-git-http-backend host>`.
+
+### Git garbage collection
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `gitGC.image` | Image name of the Git-GC container image | `k8sgerrit/git-gc` |
+| `gitGC.schedule` | Cron-formatted schedule with which to run Git garbage collection | `0 6,18 * * *` |
+| `gitGC.resources` | Configure the amount of resources the pod requests/is allowed | `requests.cpu: 100m` |
+|                   |                                                               | `requests.memory: 256Mi` |
+|                   |                                                               | `limits.cpu: 100m` |
+|                   |                                                               | `limits.memory: 256Mi` |
+| `gitGC.tolerations` | Taints and tolerations work together to ensure that pods are not scheduled onto inappropriate nodes. For more information, please refer to the following documents. [Taints and Tolerations](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration) | [] |
+| `gitGC.nodeSelector` | Assigns a Pod to the specified Nodes. For more information, please refer to the following documents. [Assign Pods to Nodes](https://kubernetes.io/docs/tasks/configure-pod-container/assign-pods-nodes/). [Assigning Pods to Nodes](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/) | {} |
+| `gitGC.affinity` | Assigns a Pod to the specified Nodes. For more information, please refer to the following documents. [Assign Pods to Nodes using Node Affinity](https://kubernetes.io/docs/tasks/configure-pod-container/assign-pods-nodes-using-node-affinity/). [Assigning Pods to Nodes](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/) | {} |
+| `gitGC.additionalPodLabels` | Additional labels for Pods | `{}` |
+
+### Gerrit replica
+
+***note
+The way the Jetty servlet used by Gerrit works, the Gerrit replica component of the
+gerrit-replica chart actually requires the URL to be known, when the chart is installed.
+The suggested way to do that is to use the provided Ingress resource. This requires
+that a URL is available and that the DNS is configured to point the URL to the
+IP of the node the Ingress controller is running on!
+***
+
+***note
+Setting the canonical web URL in the gerrit.config to the host used for the Ingress
+is mandatory, if access to the Gerrit replica is required!
+***
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `gerritReplica.images.gerritInit` | Image name of the Gerrit init container image | `k8sgerrit/gerrit-init` |
+| `gerritReplica.images.gerritReplica` | Image name of the Gerrit replica container image | `k8sgerrit/gerrit` |
+| `gerritReplica.tolerations` | Taints and tolerations work together to ensure that pods are not scheduled onto inappropriate nodes. For more information, please refer to the following documents. [Taints and Tolerations](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration) | [] |
+| `gerritReplica.topologySpreadConstraints` | Control how Pods are spread across your cluster among failure-domains. For more information, please refer to the following documents. [Pod Topology Spread Constraints](https://kubernetes.io/docs/concepts/workloads/pods/pod-topology-spread-constraints) | {} |
+| `gerritReplica.nodeSelector` | Assigns a Pod to the specified Nodes. For more information, please refer to the following documents. [Assign Pods to Nodes](https://kubernetes.io/docs/tasks/configure-pod-container/assign-pods-nodes/). [Assigning Pods to Nodes](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/) | {} |
+| `gerritReplica.affinity` | Assigns a Pod to the specified Nodes. By default, gerrit-replica is evenly distributed on `topology.kubernetes.io/zone`. For more information, please refer to the following documents. [Assign Pods to Nodes using Node Affinity](https://kubernetes.io/docs/tasks/configure-pod-container/assign-pods-nodes-using-node-affinity/). [Assigning Pods to Nodes](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/) | podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].weight: 100 |
+| | | podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].podAffinityTerm.topologyKey: "topology.kubernetes.io/zone" |
+| | | podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].podAffinityTerm.labelSelector.matchExpressions[0].key: app |
+| | | podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].podAffinityTerm.labelSelector.matchExpressions[0].operator: In |
+| | | podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].podAffinityTerm.labelSelector.matchExpressions[0].values[0]: gerrit-replica |
+| `gerritReplica.replicas` | Number of pod replicas to deploy | `1` |
+| `gerritReplica.additionalAnnotations` | Additional annotations for the Pods | {} |
+| `gerritReplica.additionalPodLabels` | Additional labels for the Pods | `{}` |
+| `gerritReplica.maxSurge` | Max. percentage or number of pods allowed to be scheduled above the desired number | `25%` |
+| `gerritReplica.maxUnavailable` | Max. percentage or number of pods allowed to be unavailable at a time | `100%` |
+| `gerritReplica.livenessProbe` | Configuration of the liveness probe timings | `{initialDelaySeconds: 60, periodSeconds: 5}` |
+| `gerritReplica.readinessProbe` | Configuration of the readiness probe timings | `{initialDelaySeconds: 10, periodSeconds: 10}` |
+| `gerritReplica.startupProbe` | Configuration of the startup probe timings | `{initialDelaySeconds: 10, periodSeconds: 5}` |
+| `gerritReplica.gracefulStopTimeout` | Time in seconds Kubernetes will wait until killing the pod during termination (has to be longer then Gerrit's httpd.gracefulStopTimeout to allow graceful shutdown of Gerrit) | `90` |
+| `gerritReplica.resources` | Configure the amount of resources the pod requests/is allowed | `requests.cpu: 1` |
+|                           |                                                               | `requests.memory: 5Gi` |
+|                           |                                                               | `limits.cpu: 1` |
+|                           |                                                               | `limits.memory: 6Gi` |
+| `gerritReplica.networkPolicy.ingress` | Custom ingress-network policy for gerrit-replica pods | `nil` |
+| `gerritReplica.networkPolicy.egress` | Custom egress-network policy for gerrit-replica pods | `nil` |
+| `gerritReplica.service.additionalAnnotations` | Additional annotations for the Service | `{}` |
+| `gerritReplica.service.loadBalancerSourceRanges` | The list of allowed IPs for the Service | `[]` |
+| `gerritReplica.service.type` | Which kind of Service to deploy | `NodePort` |
+| `gerritReplica.service.externalTrafficPolicy` | Specify how traffic from external is handled | `Cluster` |
+| `gerritReplica.service.http.port` | Port over which to expose HTTP | `80` |
+| `gerritReplica.service.ssh.enabled` | Whether to enable SSH for the Gerrit replica | `false` |
+| `gerritReplica.service.ssh.port` | Port for SSH | `29418` |
+| `gerritReplica.keystore` | base64-encoded Java keystore (`cat keystore.jks \| base64`) to be used by Gerrit, when using SSL | `nil` |
+| `gerritReplica.pluginManagement.plugins` | List of Gerrit plugins to install | `[]` |
+| `gerritReplica.pluginManagement.plugins[0].name` | Name of plugin | `nil` |
+| `gerritReplica.pluginManagement.plugins[0].url` | Download url of plugin. If given the plugin will be downloaded, otherwise it will be installed from the gerrit.war-file. | `nil` |
+| `gerritReplica.pluginManagement.plugins[0].sha1` | SHA1 sum of plugin jar used to ensure file integrity and version (optional) | `nil` |
+| `gerritReplica.pluginManagement.plugins[0].installAsLibrary` | Whether the plugin should be symlinked to the lib-dir in the Gerrit site. | `nil` |
+| `gerritReplica.pluginManagement.libs` | List of Gerrit library modules to install | `[]` |
+| `gerritReplica.pluginManagement.libs[0].name` | Name of the lib module | `nil` |
+| `gerritReplica.pluginManagement.libs[0].url` | Download url of lib module. | `nil` |
+| `gerritReplica.pluginManagement.libs[0].sha1` | SHA1 sum of plugin jar used to ensure file integrity and version | `nil` |
+| `gerritReplica.pluginManagement.cache.enabled` | Whether to cache downloaded plugins | `false` |
+| `gerritReplica.pluginManagement.cache.size` | Size of the volume used to store cached plugins | `1Gi` |
+| `gerritReplica.priorityClassName` | Name of the PriorityClass to apply to replica pods | `nil` |
+| `gerritReplica.etc.config` | Map of config files (e.g. `gerrit.config`) that will be mounted to `$GERRIT_SITE/etc`by a ConfigMap | `{gerrit.config: ..., replication.config: ...}`[see here](#Gerrit-config-files) |
+| `gerritReplica.etc.secret` | Map of config files (e.g. `secure.config`) that will be mounted to `$GERRIT_SITE/etc`by a Secret | `{secure.config: ...}` [see here](#Gerrit-config-files) |
+| `gerritReplica.additionalConfigMaps` | Allows to mount additional ConfigMaps into a subdirectory of `$SITE/data` | `[]` |
+| `gerritReplica.additionalConfigMaps[*].name` | Name of the ConfigMap | `nil` |
+| `gerritReplica.additionalConfigMaps[*].subDir` | Subdirectory under `$SITE/data` into which the files should be symlinked | `nil` |
+| `gerritReplica.additionalConfigMaps[*].data` | Data of the ConfigMap. If not set, ConfigMap has to be created manually | `nil` |
+
+### Gerrit config files
+
+The gerrit-replica chart provides a ConfigMap containing the configuration files
+used by Gerrit, e.g. `gerrit.config` and a Secret containing sensitive configuration
+like the `secure.config` to configure the Gerrit installation in the Gerrit
+component. The content of the config files can be set in the `values.yaml` under
+the keys `gerritReplica.etc.config` and `gerritReplica.etc.secret` respectively.
+The key has to be the filename (eg. `gerrit.config`) and the file's contents
+the value. This way an arbitrary number of configuration files can be loaded into
+the `$GERRIT_SITE/etc`-directory, e.g. for plugins.
+All configuration options for Gerrit are described in detail in the
+[official documentation of Gerrit](https://gerrit-review.googlesource.com/Documentation/config-gerrit.html).
+Some options however have to be set in a specified way for Gerrit to work as
+intended with the chart:
+
+- `gerrit.basePath`
+
+    Path to the directory containing the repositories. The chart mounts this
+    directory from a persistent volume to `/var/gerrit/git` in the container. For
+    Gerrit to find the correct directory, this has to be set to `git`.
+
+- `gerrit.serverId`
+
+    In Gerrit-version higher than 2.14 Gerrit needs a server ID, which is used by
+    NoteDB. Gerrit would usually generate a random ID on startup, but since the
+    gerrit.config file is read only, when mounted as a ConfigMap this fails.
+    Thus the server ID has to be set manually!
+
+- `gerrit.canonicalWebUrl`
+
+    The canonical web URL has to be set to the Ingress host.
+
+- `httpd.listenURL`
+
+    This has to be set to `proxy-http://*:8080/` or `proxy-https://*:8080`,
+    depending of TLS is enabled in the Ingress or not, otherwise the Jetty
+    servlet will run into an endless redirect loop.
+
+- `httpd.gracefulStopTimeout` / `sshd.gracefulStopTimeout`
+
+    To enable graceful shutdown of the embedded jetty server and SSHD, a timeout
+    has to be set with this option. This will be the maximum time, Gerrit will wait
+    for HTTP requests to finish before shutdown.
+
+- `container.user`
+
+    The technical user in the Gerrit replica container is called `gerrit`. Thus, this
+    value is required to be `gerrit`.
+
+- `container.replica`
+
+    Since this chart is meant to install a Gerrit replica, this naturally has to be
+    `true`.
+
+- `container.javaHome`
+
+    This has to be set to `/usr/lib/jvm/java-11-openjdk-amd64`, since this is
+    the path of the Java installation in the container.
+
+- `container.javaOptions`
+
+    The maximum heap size has to be set. And its value has to be lower than the
+    memory resource limit set for the container (e.g. `-Xmx4g`). In your calculation
+    allow memory for other components running in the container.
+
+To enable liveness- and readiness probes, the healthcheck plugin will be installed
+by default. Note, that by configuring to use a packaged or downloaded version of
+the healthcheck plugin, the configured version will take precedence over the default
+version. The plugin is by default configured to disable the `querychanges` and
+`auth` healthchecks, since the Gerrit replica does not index changes and a new
+Gerrit server will not yet necessarily have an user to validate authentication.
+
+The default configuration can be overwritten by adding the `healthcheck.config`
+file as a key-value pair to `gerritReplica.etc.config` as for every other configuration.
+
+SSH keys should be configured via the helm-chart using the `gerritReplica.etc.secret`
+map. Gerrit will create its own keys, if none are present in the site, but if
+multiple Gerrit pods are running, each Gerrit instance would have its own keys.
+Users accessing Gerrit via a load balancer would get issues due to changing
+host keys.
+
+## Upgrading the Chart
+
+To upgrade an existing installation of the gerrit-replica chart, e.g. to install
+a newer chart version or to use an updated custom `values.yaml`-file, execute
+the following command:
+
+```sh
+cd $(git rev-parse --show-toplevel)/helm-charts
+helm upgrade \
+  <release-name> \
+  ./gerrit-replica \ # path to chart
+  -f <path-to-custom-values>.yaml \
+```
+
+## Uninstalling the Chart
+
+To delete the chart from the cluster, use:
+
+```sh
+helm delete <release-name>
+```
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-replica/docs/nfs-provisioner.md b/charts/k8s-gerrit/helm-charts/gerrit-replica/docs/nfs-provisioner.md
new file mode 100644
index 0000000..e2d0806
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-replica/docs/nfs-provisioner.md
@@ -0,0 +1,64 @@
+# Installing a NFS-provisioner
+
+The Gerrit replica requires access to a persistent volume capable of running in
+`Read Write Many (RWM)`-mode to store the git repositories, since the repositories
+have to be accessed by mutiple pods. One possibility to provide such volumes
+is to install a provisioner for NFS-volumes into the same Kubernetes-cluster.
+This document will guide through the process.
+
+The [Kubernetes external-storage project](https://github.com/kubernetes-incubator/external-storage)
+provides an out-of-tree dynamic [provisioner](https://github.com/kubernetes-incubator/external-storage/tree/master/nfs)
+for NFS volumes. A chart exists for easy deployment of the project onto a
+Kubernetes cluster. The chart's sources can be found [here](https://github.com/helm/charts/tree/master/stable/nfs-server-provisioner).
+
+## Prerequisites
+
+This guide will use Helm to install the NFS-provisioner. Thus, Helm has to be
+installed.
+
+## Installing the nfs-server-provisioner chart
+
+A custom `values.yaml`-file containing a configuration tested with the
+gerrit-replica chart can be found in the `supplements/nfs`-directory in the
+gerrit-replica chart's root directory. In addition a file stating the tested
+version of the nfs-server-provisioner chart is present in the same directory.
+
+If needed, adapt the `values.yaml`-file for the nfs-server-provisioner chart
+further and then run:
+
+```sh
+cd $(git rev-parse --show-toplevel)/helm-charts/gerrit-replica/supplements/nfs
+helm install nfs \
+  stable/nfs-server-provisioner \
+  -f values.yaml \
+  --version $(cat VERSION)
+```
+
+For a description of the configuration options, refer to the
+[chart's documentation](https://github.com/helm/charts/blob/master/stable/nfs-server-provisioner/README.md).
+
+Here are some tips for configuring the nfs-server-provisioner chart to work with
+the gerrit-replica chart:
+
+- Deploying more than 1 `replica` led to some reliability issues in tests and
+  should be further tested for now, if required.
+- The name of the StorageClass created for NFS-volumes has to be the same as the
+  one defined in the gerrit-replica chart for `storageClasses.shared.name`
+- The StorageClas for NFS-volumes needs to have the parameter `mountOptions: vers=4.1`,
+  due to compatibility [issues](https://github.com/kubernetes-incubator/external-storage/issues/223)
+  with Ganesha.
+
+## Deleting the nfs-server-provisioner chart
+
+***note
+**Attention:** Never delete the nfs-server-provisioner chart, if there is still a
+PersistentVolumeClaim and Pods using a NFS-volume provisioned by the NFS server
+provisioner. This will lead to crashed pods, that will not be terminated correctly.
+***
+
+If no Pod or PVC is using a NFS-volume provisioned by the NFS server provisioner
+anymore, delete it like any other chart:
+
+```sh
+helm delete nfs
+```
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-replica/supplements/nfs/VERSION b/charts/k8s-gerrit/helm-charts/gerrit-replica/supplements/nfs/VERSION
new file mode 100644
index 0000000..7dff5b8
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-replica/supplements/nfs/VERSION
@@ -0,0 +1 @@
+0.2.1
\ No newline at end of file
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-replica/supplements/nfs/values.yaml b/charts/k8s-gerrit/helm-charts/gerrit-replica/supplements/nfs/values.yaml
new file mode 100644
index 0000000..aa3d9ce
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-replica/supplements/nfs/values.yaml
@@ -0,0 +1,42 @@
+# Deploying more than 1 `replica` led to some reliability issues in tests and
+# should be further tested for now, if required.
+replicaCount: 1
+
+image:
+  repository: quay.io/kubernetes_incubator/nfs-provisioner
+  tag: v1.0.9
+  pullPolicy: IfNotPresent
+
+service:
+  type: ClusterIP
+  nfsPort: 2049
+  mountdPort: 20048
+  rpcbindPort: 51413
+
+persistence:
+  enabled: true
+  storageClass: default
+  accessMode: ReadWriteOnce
+  size: 7.5Gi
+
+storageClass:
+  create: true
+  defaultClass: false
+  # The name of the StorageClass has to be the same as the one defined in the
+  # gerrit-replica chart for `storageClasses.shared.name`
+  name: shared-storage
+  parameters:
+    # Required!
+    mountOptions: vers=4.1
+  reclaimPolicy: Delete
+
+rbac:
+  create: true
+
+resources:
+  requests:
+    cpu: 100m
+    memory: 256Mi
+  limits:
+    cpu: 100m
+    memory: 256Mi
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/NOTES.txt b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/NOTES.txt
new file mode 100644
index 0000000..30e263f
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/NOTES.txt
@@ -0,0 +1,35 @@
+A Gerrit replica has been deployed.
+=================================
+
+The Apache-Git-HTTP-Backend is now ready to receive replication requests from the
+primary Gerrit. Please configure the replication plugin of the primary Gerrit to
+push the repositories to:
+
+{{ if .Values.istio.enabled -}}
+  http {{- if .Values.istio.tls.enabled -}} s {{- end -}} :// {{- .Values.istio.host -}} /${name}.git
+{{ else if .Values.ingress.enabled -}}
+  http {{- if .Values.ingress.tls.enabled -}} s {{- end -}} :// {{- .Values.ingress.host -}} /${name}.git
+{{- else }}
+  http://<EXTERNAL-IP>: {{- .Values.gitBackend.service.http.port -}} /${name}.git
+  The external IP of the service can be found by running:
+  kubectl get svc git-backend-service
+{{- end }}
+
+Project creation, project deletion and HEAD update can also be replicated. To enable
+this feature configure the replication plugin to use an adminUrl using the format
+`gerrit+http {{- if .Values.ingress.tls.enabled -}} s {{- end -}} :// {{- .Values.ingress.host -}}`.
+
+A detailed guide of how to configure Gerrit's replication plugin can be found here:
+
+https://gerrit.googlesource.com/plugins/replication/+doc/master/src/main/resources/Documentation/config.md
+
+The Gerrit replica is starting up.
+
+The initialization process may take some time. Afterwards the git repositories
+will be available under:
+
+{{ if .Values.istio.enabled -}}
+  http {{- if .Values.istio.tls.enabled -}} s {{- end -}} :// {{- .Values.istio.host -}} /<repository-name>.git
+{{- else }}
+  http {{- if .Values.ingress.tls.enabled -}} s {{- end -}} :// {{- .Values.ingress.host -}} /<repository-name>.git
+{{- end }}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/_helpers.tpl b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/_helpers.tpl
new file mode 100644
index 0000000..500d58c
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/_helpers.tpl
@@ -0,0 +1,20 @@
+{{/*
+Create chart name and version as used by the chart label.
+*/}}
+{{- define "gerrit-replica.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
+
+{{/*
+Create secret to access docker registry
+*/}}
+{{- define "imagePullSecret" }}
+{{- printf "{\"auths\": {\"%s\": {\"auth\": \"%s\"}}}" .Values.images.registry.name (printf "%s:%s" .Values.images.registry.ImagePullSecret.username .Values.images.registry.ImagePullSecret.password | b64enc) | b64enc }}
+{{- end }}
+
+{{/*
+Add '/' to registry if needed.
+*/}}
+{{- define "registry" -}}
+{{ if .Values.images.registry.name }}{{- printf "%s/" .Values.images.registry.name -}}{{end}}
+{{- end -}}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/gerrit-replica.configmap.yaml b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/gerrit-replica.configmap.yaml
new file mode 100644
index 0000000..1aa9496
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/gerrit-replica.configmap.yaml
@@ -0,0 +1,78 @@
+{{- $root := . -}}
+
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: {{ .Release.Name }}-gerrit-replica-configmap
+  labels:
+    app.kubernetes.io/component: gerrit-replica
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit-replica.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+data:
+  {{- range $key, $value := .Values.gerritReplica.etc.config }}
+  {{ $key }}:
+{{ toYaml $value | indent 4 }}
+  {{- end }}
+  {{- if not (hasKey .Values.gerritReplica.etc.config "healthcheck.config") }}
+  healthcheck.config: |-
+    [healthcheck "auth"]
+      # On new instances there may be no users to use for healthchecks
+      enabled = false
+    [healthcheck "querychanges"]
+      # On new instances there won't be any changes to query
+      enabled = false
+  {{- end }}
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: {{ .Release.Name }}-gerrit-init-configmap
+  labels:
+    app.kubernetes.io/component: gerrit-replica
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit-replica.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+data:
+  gerrit-init.yaml: |-
+    {{ if .Values.caCert -}}
+    caCertPath: /var/config/ca.crt
+    {{- end }}
+    pluginCacheEnabled: {{ .Values.gerritReplica.pluginManagement.cache.enabled }}
+    pluginCacheDir: /var/mnt/plugins
+    {{- if .Values.gerritReplica.pluginManagement.plugins }}
+    plugins:
+{{ toYaml .Values.gerritReplica.pluginManagement.plugins | indent 6}}
+    {{- end }}
+    {{- if .Values.gerritReplica.pluginManagement.libs }}
+    libs:
+{{ toYaml .Values.gerritReplica.pluginManagement.libs | indent 6}}
+    {{- end }}
+{{- range .Values.gerritReplica.additionalConfigMaps -}}
+{{- if .data }}
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name:  {{ $root.Release.Name }}-{{ .name }}
+  labels:
+    app.kubernetes.io/component: gerrit-replica
+    app.kubernetes.io/instance: {{ $root.Release.Name }}
+    chart: {{ template "gerrit-replica.chart" $root }}
+    heritage: {{ $root.Release.Service }}
+    release: {{ $root.Release.Name }}
+    {{- if $root.Values.additionalLabels }}
+{{ toYaml $root.Values.additionalLabels  | indent 4 }}
+    {{- end }}
+data:
+{{ toYaml .data | indent 2 }}
+{{- end }}
+{{- end }}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/gerrit-replica.secrets.yaml b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/gerrit-replica.secrets.yaml
new file mode 100644
index 0000000..ece9b9a
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/gerrit-replica.secrets.yaml
@@ -0,0 +1,21 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  name:  {{ .Release.Name }}-gerrit-replica-secure-config
+  labels:
+    app.kubernetes.io/component: gerrit-replica
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit-replica.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+data:
+  {{ if .Values.gerritReplica.keystore -}}
+  keystore: {{ .Values.gerritReplica.keystore }}
+  {{- end }}
+  {{- range $key, $value := .Values.gerritReplica.etc.secret }}
+  {{ $key }}: {{ $value | b64enc }}
+  {{- end }}
+type: Opaque
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/gerrit-replica.service.yaml b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/gerrit-replica.service.yaml
new file mode 100644
index 0000000..01030b4
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/gerrit-replica.service.yaml
@@ -0,0 +1,40 @@
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ .Release.Name }}-gerrit-replica-service
+  labels:
+    app.kubernetes.io/component: gerrit-replica
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit-replica.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+  {{- if .Values.gerritReplica.service.additionalAnnotations }}
+  annotations:
+{{ toYaml .Values.gerritReplica.service.additionalAnnotations  | indent 4 }}
+  {{- end }}
+spec:
+  {{ with .Values.gerritReplica.service }}
+  {{- if .loadBalancerSourceRanges -}}
+  loadBalancerSourceRanges:
+{{- range .loadBalancerSourceRanges }}
+    - {{ . | quote }}
+{{- end }}
+  {{- end }}
+  ports:
+  - name: http
+    port: {{ .http.port }}
+    targetPort: 8080
+  {{ if .ssh.enabled -}}
+  - name: ssh
+    port: {{ .ssh.port }}
+    targetPort: 29418
+  {{- end }}
+  type: {{ .type }}
+  externalTrafficPolicy: {{ .externalTrafficPolicy }}
+  {{- end }}
+  selector:
+    app.kubernetes.io/component: gerrit-replica
+    app.kubernetes.io/instance: {{ .Release.Name }}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/gerrit-replica.stateful-set.yaml b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/gerrit-replica.stateful-set.yaml
new file mode 100644
index 0000000..d4d74a9
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/gerrit-replica.stateful-set.yaml
@@ -0,0 +1,337 @@
+{{- $root := . -}}
+
+apiVersion: apps/v1
+kind: StatefulSet
+metadata:
+  name: {{ .Release.Name }}-gerrit-replica-statefulset
+  labels:
+    app.kubernetes.io/component: gerrit-replica
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit-replica.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+spec:
+  serviceName: {{ .Release.Name }}-gerrit-replica-service
+  replicas: {{ .Values.gerritReplica.replicas }}
+  updateStrategy:
+    rollingUpdate:
+      partition: {{ .Values.gerritReplica.updatePartition }}
+  selector:
+    matchLabels:
+      app.kubernetes.io/component: gerrit-replica
+      app.kubernetes.io/instance: {{ .Release.Name }}
+  template:
+    metadata:
+      labels:
+        app.kubernetes.io/component: gerrit-replica
+        app.kubernetes.io/instance: {{ .Release.Name }}
+        chart: {{ template "gerrit-replica.chart" . }}
+        heritage: {{ .Release.Service }}
+        release: {{ .Release.Name }}
+        {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 8 }}
+        {{- end }}
+        {{- if .Values.gerritReplica.additionalPodLabels }}
+{{ toYaml .Values.gerritReplica.additionalPodLabels  | indent 8 }}
+        {{- end }}
+      annotations:
+        chartRevision: "{{ .Release.Revision }}"
+        {{- if .Values.gerritReplica.additionalAnnotations }}
+{{ toYaml .Values.gerritReplica.additionalAnnotations  | indent 8 }}
+        {{- end }}
+    spec:
+      {{- with .Values.gerritReplica.tolerations }}
+      tolerations:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      {{- with .Values.gerritReplica.topologySpreadConstraints }}
+      topologySpreadConstraints:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      {{- with .Values.gerritReplica.nodeSelector }}
+      nodeSelector:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      {{- with .Values.gerritReplica.affinity }}
+      affinity:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      {{- with .Values.gerritReplica.priorityClassName }}
+      priorityClassName: {{ . }}
+      {{- end }}
+      terminationGracePeriodSeconds: {{ .Values.gerritReplica.gracefulStopTimeout }}
+      securityContext:
+        fsGroup: 100
+      {{ if .Values.images.registry.ImagePullSecret.name -}}
+      imagePullSecrets:
+      - name: {{ .Values.images.registry.ImagePullSecret.name }}
+      {{- range .Values.images.additionalImagePullSecrets }}
+      - name: {{ . }}
+      {{- end }}
+      {{- end }}
+      initContainers:
+      {{- if and .Values.nfsWorkaround.enabled .Values.nfsWorkaround.chownOnStartup }}
+      - name: nfs-init
+        image: {{ .Values.images.busybox.registry -}}/busybox:{{- .Values.images.busybox.tag }}
+        command:
+        - sh
+        - -c
+        args:
+        - |
+          chown 1000:100 /var/mnt/logs
+          chown 1000:100 /var/mnt/git
+        env:
+        - name: POD_NAME
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.name
+        volumeMounts:
+        - name: logs
+          subPathExpr: "gerrit-replica/$(POD_NAME)"
+          mountPath: "/var/mnt/logs"
+        - name: git-repositories
+          mountPath: "/var/mnt/git"
+        {{- if .Values.nfsWorkaround.idDomain }}
+        - name: nfs-config
+          mountPath: "/etc/idmapd.conf"
+          subPath: idmapd.conf
+        {{- end }}
+      {{- end }}
+      - name: gerrit-init
+        image: {{ template "registry" . }}{{ .Values.gerritReplica.images.gerritInit }}:{{ .Values.images.version }}
+        imagePullPolicy: {{ .Values.images.imagePullPolicy }}
+        env:
+        - name: POD_NAME
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.name
+        volumeMounts:
+        - name: gerrit-site
+          mountPath: "/var/gerrit"
+        - name: git-repositories
+          mountPath: "/var/mnt/git"
+        - name: logs
+          subPathExpr: "gerrit-replica/$(POD_NAME)"
+          mountPath: "/var/mnt/logs"
+        - name: gerrit-init-config
+          mountPath: "/var/config/gerrit-init.yaml"
+          subPath: gerrit-init.yaml
+        {{- if and .Values.nfsWorkaround.enabled .Values.nfsWorkaround.idDomain }}
+        - name: nfs-config
+          mountPath: "/etc/idmapd.conf"
+          subPath: idmapd.conf
+        {{- end }}
+        {{- if and .Values.gerritReplica.pluginManagement.cache.enabled }}
+        - name: gerrit-plugin-cache
+          mountPath: "/var/mnt/plugins"
+        {{- end }}
+        - name: gerrit-config
+          mountPath: "/var/mnt/etc/config"
+        - name: gerrit-replica-secure-config
+          mountPath: "/var/mnt/etc/secret"
+        {{ if .Values.caCert -}}
+        - name: tls-ca
+          subPath: ca.crt
+          mountPath: "/var/config/ca.crt"
+        {{- end }}
+        {{- range .Values.gerritReplica.additionalConfigMaps }}
+        - name: {{ .name }}
+          mountPath: "/var/mnt/data/{{ .subDir }}"
+        {{- end }}
+      containers:
+      - name: gerrit-replica
+        image: {{ template "registry" . }}{{ .Values.gerritReplica.images.gerritReplica }}:{{ .Values.images.version }}
+        imagePullPolicy: {{ .Values.images.imagePullPolicy }}
+        env:
+        - name: POD_NAME
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.name
+        lifecycle:
+          preStop:
+            exec:
+              command:
+                - "/bin/ash"
+                - "-c"
+                - "kill -2 $(pidof java) && tail --pid=$(pidof java) -f /dev/null"
+        ports:
+        - name: http
+          containerPort: 8080
+        {{ if .Values.gerritReplica.service.ssh -}}
+        - name: ssh
+          containerPort: 29418
+        {{- end }}
+        volumeMounts:
+        - name: gerrit-site
+          mountPath: "/var/gerrit"
+        - name: git-repositories
+          mountPath: "/var/mnt/git"
+        - name: logs
+          subPathExpr: "gerrit-replica/$(POD_NAME)"
+          mountPath: "/var/mnt/logs"
+        {{- if and .Values.nfsWorkaround.enabled .Values.nfsWorkaround.idDomain }}
+        - name: nfs-config
+          mountPath: "/etc/idmapd.conf"
+          subPath: idmapd.conf
+        {{- end }}
+        - name: gerrit-config
+          mountPath: "/var/mnt/etc/config"
+        - name: gerrit-replica-secure-config
+          mountPath: "/var/mnt/etc/secret"
+        {{- range .Values.gerritReplica.additionalConfigMaps }}
+        - name: {{ .name }}
+          mountPath: "/var/mnt/data/{{ .subDir }}"
+        {{- end }}
+        livenessProbe:
+          httpGet:
+            path: /config/server/healthcheck~status
+            port: http
+{{ toYaml .Values.gerritReplica.livenessProbe | indent 10 }}
+        readinessProbe:
+          httpGet:
+            path: /config/server/healthcheck~status
+            port: http
+{{ toYaml .Values.gerritReplica.readinessProbe | indent 10 }}
+        startupProbe:
+          httpGet:
+            path: /config/server/healthcheck~status
+            port: http
+{{ toYaml .Values.gerritReplica.startupProbe | indent 10 }}
+        resources:
+{{ toYaml .Values.gerritReplica.resources | indent 10 }}
+      {{ if .Values.istio.enabled -}}
+      - name: istio-proxy
+        image: auto
+        lifecycle:
+          preStop:
+            exec:
+              command:
+              - "/bin/sh"
+              - "-c"
+              - "while [ $(netstat -plunt | grep tcp | grep -v envoy | wc -l | xargs) -ne 0 ]; do sleep 1; done"
+      {{- end }}
+      {{ if .Values.promtailSidecar.enabled -}}
+      - name: promtail
+        image: {{ .Values.promtailSidecar.image }}:v{{ .Values.promtailSidecar.version }}
+        imagePullPolicy: {{ .Values.images.imagePullPolicy }}
+        command:
+        - sh
+        - -ec
+        args:
+        - |-
+          /usr/bin/promtail \
+            -config.file=/etc/promtail/promtail.yaml \
+            -client.url={{ .Values.promtailSidecar.loki.url }}/loki/api/v1/push \
+            -client.external-labels=instance=$HOSTNAME
+        env:
+        - name: POD_NAME
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.name
+        resources:
+{{ toYaml .Values.promtailSidecar.resources | indent 10 }}
+        volumeMounts:
+        - name: promtail-config
+          mountPath: /etc/promtail/promtail.yaml
+          subPath: promtail.yaml
+        - name: promtail-secret
+          mountPath: /etc/promtail/promtail.secret
+          subPath: promtail.secret
+        {{- if not .Values.promtailSidecar.tls.skipVerify }}
+        - name: tls-ca
+          mountPath: /etc/promtail/promtail.ca.crt
+          subPath: ca.crt
+        {{- end }}
+        - name: logs
+          subPathExpr: "gerrit-replica/$(POD_NAME)"
+          mountPath: "/var/gerrit/logs"
+        {{- if and .Values.nfsWorkaround.enabled .Values.nfsWorkaround.idDomain }}
+        - name: nfs-config
+          mountPath: "/etc/idmapd.conf"
+          subPath: idmapd.conf
+        {{- end }}
+      {{- end }}
+      volumes:
+      {{ if not .Values.gerritReplica.persistence.enabled -}}
+      - name: gerrit-site
+        emptyDir: {}
+      {{- end }}
+      {{- if and .Values.gerritReplica.pluginManagement.cache.enabled }}
+      - name: gerrit-plugin-cache
+        persistentVolumeClaim:
+          claimName: {{ .Release.Name }}-plugin-cache-pvc
+      {{- end }}
+      - name: git-repositories
+        persistentVolumeClaim:
+          {{- if .Values.gitRepositoryStorage.externalPVC.use }}
+          claimName: {{ .Values.gitRepositoryStorage.externalPVC.name }}
+          {{- else }}
+          claimName: {{ .Release.Name }}-git-repositories-pvc
+          {{- end }}
+      - name: logs
+        {{ if .Values.logStorage.enabled -}}
+        persistentVolumeClaim:
+          {{- if .Values.logStorage.externalPVC.use }}
+          claimName: {{ .Values.logStorage.externalPVC.name }}
+          {{- else }}
+          claimName: {{ .Release.Name }}-log-pvc
+          {{- end }}
+        {{ else -}}
+        emptyDir: {}
+        {{- end }}
+      - name: gerrit-init-config
+        configMap:
+          name: {{ .Release.Name }}-gerrit-init-configmap
+      - name: gerrit-config
+        configMap:
+          name: {{ .Release.Name }}-gerrit-replica-configmap
+      - name: gerrit-replica-secure-config
+        secret:
+          secretName: {{ .Release.Name }}-gerrit-replica-secure-config
+      {{ if .Values.caCert -}}
+      - name: tls-ca
+        secret:
+          secretName: {{ .Release.Name }}-tls-ca
+      {{- end }}
+      {{- range .Values.gerritReplica.additionalConfigMaps }}
+      - name: {{ .name }}
+        configMap:
+          name: {{ if .data }}{{ $root.Release.Name }}-{{ .name }}{{ else }}{{ .name }}{{ end }}
+      {{- end }}
+      {{- if and .Values.nfsWorkaround.enabled .Values.nfsWorkaround.idDomain }}
+      - name: nfs-config
+        configMap:
+          name: {{ .Release.Name }}-nfs-configmap
+      {{- end }}
+      {{ if .Values.promtailSidecar.enabled -}}
+      - name: promtail-config
+        configMap:
+          name: {{ .Release.Name }}-promtail-gerrit-configmap
+      - name: promtail-secret
+        secret:
+          secretName: {{ .Release.Name }}-promtail-secret
+      {{- end }}
+  {{ if .Values.gerritReplica.persistence.enabled -}}
+  volumeClaimTemplates:
+  - metadata:
+      name: gerrit-site
+      labels:
+        app.kubernetes.io/component: gerrit-replica
+        app.kubernetes.io/instance: {{ .Release.Name }}
+        chart: {{ template "gerrit-replica.chart" . }}
+        heritage: {{ .Release.Service }}
+        release: {{ .Release.Name }}
+        {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 8 }}
+        {{- end }}
+    spec:
+      accessModes:
+      - ReadWriteOnce
+      resources:
+        requests:
+          storage: {{ .Values.gerritReplica.persistence.size }}
+      storageClassName: {{ .Values.storageClasses.default.name }}
+  {{- end }}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/gerrit-replica.storage.yaml b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/gerrit-replica.storage.yaml
new file mode 100644
index 0000000..c710737
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/gerrit-replica.storage.yaml
@@ -0,0 +1,22 @@
+{{- if and .Values.gerritReplica.pluginManagement.cache.enabled }}
+kind: PersistentVolumeClaim
+apiVersion: v1
+metadata:
+  name: {{ .Release.Name }}-plugin-cache-pvc
+  labels:
+    app.kubernetes.io/component: gerrit-replica
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit-replica.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+spec:
+  accessModes:
+  - ReadWriteMany
+  resources:
+    requests:
+      storage: {{ .Values.gerritReplica.pluginManagement.cache.size }}
+  storageClassName: {{ .Values.storageClasses.shared.name }}
+{{- end }}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/git-backend.deployment.yaml b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/git-backend.deployment.yaml
new file mode 100644
index 0000000..037bcb9
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/git-backend.deployment.yaml
@@ -0,0 +1,168 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{ .Release.Name }}-git-backend-deployment
+  labels:
+    app.kubernetes.io/component: git-backend
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit-replica.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+spec:
+  replicas: {{ .Values.gitBackend.replicas }}
+  strategy:
+    rollingUpdate:
+      maxSurge: {{ .Values.gitBackend.maxSurge }}
+      maxUnavailable: {{ .Values.gitBackend.maxUnavailable }}
+  selector:
+    matchLabels:
+      app.kubernetes.io/component: git-backend
+      app.kubernetes.io/instance: {{ .Release.Name }}
+  template:
+    metadata:
+      labels:
+        app.kubernetes.io/component: git-backend
+        app.kubernetes.io/instance: {{ .Release.Name }}
+        chart: {{ template "gerrit-replica.chart" . }}
+        heritage: {{ .Release.Service }}
+        release: {{ .Release.Name }}
+        {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 8 }}
+        {{- end }}
+        {{- if .Values.gitBackend.additionalPodLabels }}
+{{ toYaml .Values.gitBackend.additionalPodLabels  | indent 8 }}
+        {{- end }}
+      annotations:
+        chartRevision: "{{ .Release.Revision }}"
+    spec:
+      {{- with .Values.gitBackend.tolerations }}
+      tolerations:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      {{- with .Values.gitBackend.topologySpreadConstraints }}
+      topologySpreadConstraints:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      {{- with .Values.gitBackend.nodeSelector }}
+      nodeSelector:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      {{- with .Values.gitBackend.affinity }}
+      affinity:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      securityContext:
+        fsGroup: 100
+      {{ if .Values.images.registry.ImagePullSecret.name -}}
+      imagePullSecrets:
+      - name: {{ .Values.images.registry.ImagePullSecret.name }}
+      {{- range .Values.images.additionalImagePullSecrets }}
+      - name: {{ . }}
+      {{- end }}
+      {{- end }}
+      initContainers:
+      {{- if and .Values.nfsWorkaround.enabled .Values.nfsWorkaround.chownOnStartup }}
+      - name: nfs-init
+        image: {{ .Values.images.busybox.registry -}}/busybox:{{- .Values.images.busybox.tag }}
+        command:
+        - sh
+        - -c
+        args:
+        - |
+          chown 1000:100 /var/mnt/logs
+          chown 1000:100 /var/mnt/git
+        env:
+        - name: POD_NAME
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.name
+        volumeMounts:
+        - name: logs
+          subPathExpr: "gerrit-replica/$(POD_NAME)"
+          mountPath: "/var/mnt/logs"
+        - name: git-repositories
+          mountPath: "/var/mnt/git"
+        {{- if .Values.nfsWorkaround.idDomain }}
+        - name: nfs-config
+          mountPath: "/etc/idmapd.conf"
+          subPath: idmapd.conf
+        {{- end }}
+      {{- end }}
+      containers:
+      - name: apache-git-http-backend
+        imagePullPolicy: {{ .Values.images.imagePullPolicy }}
+        image: {{ template "registry" . }}{{ .Values.gitBackend.image }}:{{ .Values.images.version }}
+        env:
+        - name: POD_NAME
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.name
+        ports:
+        - name: http-port
+          containerPort: 80
+        resources:
+{{ toYaml .Values.gitBackend.resources | indent 10 }}
+        livenessProbe:
+          tcpSocket:
+            port: http-port
+{{ toYaml .Values.gitBackend.livenessProbe | indent 10 }}
+        readinessProbe:
+          tcpSocket:
+            port: http-port
+{{ toYaml .Values.gitBackend.readinessProbe | indent 10 }}
+        volumeMounts:
+        - name: git-repositories
+          mountPath: "/var/gerrit/git"
+        - name: logs
+          subPathExpr: "apache-git-http-backend/$(POD_NAME)"
+          mountPath: "/var/log/apache2"
+        {{- if and .Values.nfsWorkaround.enabled .Values.nfsWorkaround.idDomain }}
+        - name: nfs-config
+          mountPath: "/etc/idmapd.conf"
+          subPath: idmapd.conf
+        {{- end }}
+        - name: git-backend-secret
+          readOnly: true
+          subPath: .htpasswd
+          mountPath: "/var/apache/credentials/.htpasswd"
+      {{ if .Values.istio.enabled -}}
+      - name: istio-proxy
+        image: auto
+        lifecycle:
+          preStop:
+            exec:
+              command:
+              - "/bin/sh"
+              - "-c"
+              - "while [ $(netstat -plunt | grep tcp | grep -v envoy | wc -l | xargs) -ne 0 ]; do sleep 1; done"
+      {{- end }}
+      volumes:
+      - name: git-repositories
+        persistentVolumeClaim:
+          {{- if .Values.gitRepositoryStorage.externalPVC.use }}
+          claimName: {{ .Values.gitRepositoryStorage.externalPVC.name }}
+          {{- else }}
+          claimName: {{ .Release.Name }}-git-repositories-pvc
+          {{- end }}
+      - name: git-backend-secret
+        secret:
+          secretName: {{ .Release.Name }}-git-backend-secret
+      - name: logs
+        {{ if .Values.logStorage.enabled -}}
+        persistentVolumeClaim:
+          {{- if .Values.logStorage.externalPVC.use }}
+          claimName: {{ .Values.logStorage.externalPVC.name }}
+          {{- else }}
+          claimName: {{ .Release.Name }}-log-pvc
+          {{- end }}
+        {{ else -}}
+        emptyDir: {}
+        {{- end }}
+      {{- if and .Values.nfsWorkaround.enabled .Values.nfsWorkaround.idDomain }}
+      - name: nfs-config
+        configMap:
+          name: {{ .Release.Name }}-nfs-configmap
+      {{- end }}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/git-backend.secrets.yaml b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/git-backend.secrets.yaml
new file mode 100644
index 0000000..94b1705
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/git-backend.secrets.yaml
@@ -0,0 +1,16 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  name:  {{ .Release.Name }}-git-backend-secret
+  labels:
+    app.kubernetes.io/component: git-backend
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit-replica.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+data:
+  .htpasswd: {{ required "A .htpasswd-file is required for the git backend." .Values.gitBackend.credentials.htpasswd | b64enc }}
+type: Opaque
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/git-backend.service.yaml b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/git-backend.service.yaml
new file mode 100644
index 0000000..7bd47ef
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/git-backend.service.yaml
@@ -0,0 +1,35 @@
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ .Release.Name }}-git-backend-service
+  labels:
+    app.kubernetes.io/component: git-backend
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit-replica.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+  {{- if .Values.gitBackend.service.additionalAnnotations }}
+  annotations:
+{{ toYaml .Values.gitBackend.service.additionalAnnotations  | indent 4 }}
+  {{- end }}
+spec:
+  {{ with .Values.gitBackend.service }}
+  {{- if .loadBalancerSourceRanges -}}
+  loadBalancerSourceRanges:
+{{- range .loadBalancerSourceRanges }}
+    - {{ . | quote }}
+{{- end }}
+  {{- end }}
+  ports:
+  - name: http
+    port: {{ .http.port }}
+    targetPort: 80
+  type: {{ .type }}
+  externalTrafficPolicy: {{ .externalTrafficPolicy }}
+  {{- end }}
+  selector:
+    app.kubernetes.io/component: git-backend
+    app.kubernetes.io/instance: {{ .Release.Name }}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/git-gc.cronjob.yaml b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/git-gc.cronjob.yaml
new file mode 100644
index 0000000..028ffe9
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/git-gc.cronjob.yaml
@@ -0,0 +1,134 @@
+apiVersion: batch/v1
+kind: CronJob
+metadata:
+  name:  {{ .Release.Name }}-git-gc
+  labels:
+    app.kubernetes.io/component: git-gc
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit-replica.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+spec:
+  schedule: {{ .Values.gitGC.schedule | quote }}
+  concurrencyPolicy: "Forbid"
+  jobTemplate:
+    spec:
+      template:
+        metadata:
+          annotations:
+            cluster-autoscaler.kubernetes.io/safe-to-evict: "false"
+          {{ if .Values.istio.enabled }}
+            sidecar.istio.io/inject: "false"
+          {{- end }}
+          labels:
+            app.kubernetes.io/component: git-gc
+            app.kubernetes.io/instance: {{ .Release.Name }}
+            chart: {{ template "gerrit-replica.chart" . }}
+            heritage: {{ .Release.Service }}
+            release: {{ .Release.Name }}
+            {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 12 }}
+            {{- end }}
+            {{- if .Values.gitGC.additionalPodLabels }}
+{{ toYaml .Values.gitGC.additionalPodLabels  | indent 12 }}
+            {{- end }}
+        spec:
+          {{- with .Values.gitGC.tolerations }}
+          tolerations:
+            {{- toYaml . | nindent 10 }}
+          {{- end }}
+          {{- with .Values.gitGC.nodeSelector }}
+          nodeSelector:
+            {{- toYaml . | nindent 12 }}
+          {{- end }}
+          {{- with .Values.gitGC.affinity }}
+          affinity:
+            {{- toYaml . | nindent 12 }}
+          {{- end }}
+          restartPolicy: OnFailure
+          securityContext:
+            fsGroup: 100
+          {{ if .Values.images.registry.ImagePullSecret.name -}}
+          imagePullSecrets:
+          - name: {{ .Values.images.registry.ImagePullSecret.name }}
+          {{- range .Values.images.additionalImagePullSecrets }}
+          - name: {{ . }}
+          {{- end }}
+          {{- end }}
+          initContainers:
+          {{- if and .Values.nfsWorkaround.enabled .Values.nfsWorkaround.chownOnStartup }}
+          - name: nfs-init
+            image: {{ .Values.images.busybox.registry -}}/busybox:{{- .Values.images.busybox.tag }}
+            command:
+            - sh
+            - -c
+            args:
+            - |
+              chown 1000:100 /var/mnt/logs
+              chown 1000:100 /var/mnt/git
+            env:
+            - name: POD_NAME
+              valueFrom:
+                fieldRef:
+                  fieldPath: metadata.name
+            volumeMounts:
+            - name: logs
+              subPathExpr: "git-gc/$(POD_NAME)"
+              mountPath: "/var/mnt/logs"
+            - name: git-repositories
+              mountPath: "/var/mnt/git"
+            {{- if .Values.nfsWorkaround.idDomain }}
+            - name: nfs-config
+              mountPath: "/etc/idmapd.conf"
+              subPath: idmapd.conf
+            {{- end }}
+          {{- end }}
+          containers:
+          - name: git-gc
+            imagePullPolicy: {{ .Values.images.imagePullPolicy }}
+            image: {{ template "registry" . }}{{ .Values.gitGC.image }}:{{ .Values.images.version }}
+            resources:
+{{ toYaml .Values.gitGC.resources | indent 14 }}
+            env:
+            - name: POD_NAME
+              valueFrom:
+                fieldRef:
+                  fieldPath: metadata.name
+            volumeMounts:
+            - name: git-repositories
+              mountPath: "/var/gerrit/git"
+            - name: logs
+              subPathExpr: "git-gc/$(POD_NAME)"
+              mountPath: "/var/log/git"
+            {{- if and .Values.nfsWorkaround.enabled .Values.nfsWorkaround.idDomain }}
+            - name: nfs-config
+              mountPath: "/etc/idmapd.conf"
+              subPath: idmapd.conf
+            {{- end }}
+          volumes:
+          - name: git-repositories
+            persistentVolumeClaim:
+              {{- if .Values.gitRepositoryStorage.externalPVC.use }}
+              claimName: {{ .Values.gitRepositoryStorage.externalPVC.name }}
+              {{- else }}
+              claimName: {{ .Release.Name }}-git-repositories-pvc
+              {{- end }}
+          - name: logs
+            {{ if .Values.logStorage.enabled -}}
+            persistentVolumeClaim:
+              {{- if .Values.logStorage.externalPVC.use }}
+              claimName: {{ .Values.logStorage.externalPVC.name }}
+              {{- else }}
+              claimName: {{ .Release.Name }}-log-pvc
+              {{- end }}
+            {{ else -}}
+            emptyDir: {}
+            {{- end }}
+          {{- if and .Values.nfsWorkaround.enabled .Values.nfsWorkaround.idDomain }}
+          - name: nfs-config
+            configMap:
+              name: {{ .Release.Name }}-nfs-configmap
+          {{- end }}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/global.secrets.yaml b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/global.secrets.yaml
new file mode 100644
index 0000000..7dfe4a1
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/global.secrets.yaml
@@ -0,0 +1,18 @@
+{{ if .Values.caCert -}}
+apiVersion: v1
+kind: Secret
+metadata:
+  name:  {{ .Release.Name }}-tls-ca
+  labels:
+    app.kubernetes.io/component: gerrit-replica
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit-replica.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+data:
+  ca.crt: {{ .Values.caCert | b64enc }}
+type: Opaque
+{{- end }}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/image-pull.secret.yaml b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/image-pull.secret.yaml
new file mode 100644
index 0000000..3f97cd0
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/image-pull.secret.yaml
@@ -0,0 +1,13 @@
+{{ if and .Values.images.registry.ImagePullSecret.name .Values.images.registry.ImagePullSecret.create -}}
+apiVersion: v1
+kind: Secret
+metadata:
+  name: {{ .Values.images.registry.ImagePullSecret.name }}
+  labels:
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+type: kubernetes.io/dockerconfigjson
+data:
+  .dockerconfigjson: {{ template "imagePullSecret" . }}
+{{- end }}
\ No newline at end of file
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/ingress.yaml b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/ingress.yaml
new file mode 100644
index 0000000..e78dfcc
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/ingress.yaml
@@ -0,0 +1,86 @@
+{{ if and .Values.ingress.enabled (not .Values.istio.enabled) -}}
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: {{ .Release.Name }}-ingress
+  labels:
+    app.kubernetes.io/component: gerrit-replica
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit-replica.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+  annotations:
+    kubernetes.io/ingress.class: nginx
+    nginx.ingress.kubernetes.io/proxy-body-size: {{ .Values.ingress.maxBodySize | default "50m" }}
+    nginx.ingress.kubernetes.io/use-regex: "true"
+    nginx.ingress.kubernetes.io/configuration-snippet: |-
+      if ($args ~ service=git-receive-pack){
+        set $proxy_upstream_name "{{ .Release.Namespace }}-{{ .Release.Name }}-git-backend-service-http";
+        set $proxy_host $proxy_upstream_name;
+        set $service_name "{{ .Release.Name }}-git-backend-service";
+      }
+    {{- if .Values.ingress.additionalAnnotations }}
+{{ toYaml .Values.ingress.additionalAnnotations  | indent 4 }}
+    {{- end }}
+spec:
+  {{ if .Values.ingress.tls.enabled -}}
+  tls:
+  - hosts:
+    - {{ .Values.ingress.host }}
+    {{ if .Values.ingress.tls.secret.create -}}
+    secretName: {{ .Release.Name }}-tls-secret
+    {{- else }}
+    secretName: {{ .Values.ingress.tls.secret.name }}
+    {{- end }}
+  {{- end }}
+  rules:
+  - host: {{required "A host URL is required for the ingress. Please set 'ingress.host'" .Values.ingress.host }}
+    http:
+      paths:
+      - pathType: Prefix
+        path: /a/projects
+        backend:
+          service:
+            name: {{ .Release.Name }}-git-backend-service
+            port:
+              number: {{ .Values.gitBackend.service.http.port }}
+      - pathType: Prefix
+        path: "/.*/git-receive-pack"
+        backend:
+          service:
+            name: {{ .Release.Name }}-git-backend-service
+            port:
+              number: {{ .Values.gitBackend.service.http.port }}
+      - pathType: Prefix
+        path: /
+        backend:
+          service:
+            name: {{ .Release.Name }}-gerrit-replica-service
+            port:
+              number: {{ .Values.gerritReplica.service.http.port }}
+{{- end }}
+---
+{{ if and (and .Values.ingress.tls.enabled .Values.ingress.tls.secret.create) (not .Values.istio.enabled) -}}
+apiVersion: v1
+kind: Secret
+metadata:
+  name:  {{ .Release.Name }}-tls-secret
+  labels:
+    app.kubernetes.io/component: gerrit-replica
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit-replica.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+type: kubernetes.io/tls
+data:
+  {{ with .Values.ingress.tls -}}
+  tls.crt: {{ .cert | b64enc }}
+  tls.key: {{ .key | b64enc }}
+  {{- end }}
+{{- end }}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/istio.ingressgateway.yaml b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/istio.ingressgateway.yaml
new file mode 100644
index 0000000..3cb30c6
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/istio.ingressgateway.yaml
@@ -0,0 +1,144 @@
+{{ if .Values.istio.enabled -}}
+{{ if and .Values.istio.tls.enabled .Values.istio.tls.secret.create }}
+apiVersion: v1
+kind: Secret
+metadata:
+  name:  {{ .Release.Name }}-istio-tls-secret
+  namespace: istio-system
+  labels:
+    app.kubernetes.io/component: gerrit-replica
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit-replica.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+type: kubernetes.io/tls
+data:
+  {{ with .Values.istio.tls -}}
+  tls.crt: {{ .cert | b64enc }}
+  tls.key: {{ .key | b64enc }}
+  {{- end }}
+{{- end }}
+---
+apiVersion: networking.istio.io/v1alpha3
+kind: Gateway
+metadata:
+  name: {{ .Release.Name }}-istio-gateway
+  labels:
+    app.kubernetes.io/component: gerrit-replica
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit-replica.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+spec:
+  selector:
+    istio: ingressgateway
+  servers:
+  - port:
+      number: 80
+      name: http
+      protocol: HTTP
+    hosts:
+    - {{ .Values.istio.host }}
+  {{ if .Values.istio.tls.enabled }}
+    tls:
+      httpsRedirect: true
+  - port:
+      number: 443
+      name: https
+      protocol: HTTPS
+    hosts:
+    - {{ .Values.istio.host }}
+    tls:
+      mode: SIMPLE
+      {{ if .Values.istio.tls.secret.create }}
+      credentialName: {{ .Release.Name }}-istio-tls-secret
+      {{- else  }}
+      credentialName: {{ .Values.istio.tls.secret.name }}
+      {{- end }}
+  {{- end }}
+  {{ if .Values.istio.ssh.enabled }}
+  - port:
+      number: 29418
+      name: ssh
+      protocol: TCP
+    hosts:
+    - {{ .Values.istio.host }}
+  {{- end }}
+---
+apiVersion: networking.istio.io/v1alpha3
+kind: VirtualService
+metadata:
+  name: {{ .Release.Name }}-istio-virtual-service
+  labels:
+    app.kubernetes.io/component: gerrit-replica
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit-replica.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+spec:
+  hosts:
+  - {{ .Values.istio.host }}
+  gateways:
+  - {{ .Release.Name }}-istio-gateway
+  http:
+  - name: apache-git-http-backend
+    match:
+    - uri:
+        prefix: "/a/projects/"
+    - uri:
+        regex: "^/(.*)/git-receive-pack$"
+    - uri:
+        regex: "^/(.*)/info/refs$"
+      queryParams:
+        service:
+          exact: git-receive-pack
+    route:
+    - destination:
+        host: {{ .Release.Name }}-git-backend-service.{{ .Release.Namespace }}.svc.cluster.local
+        port:
+          number: 80
+  - name: gerrit-replica
+    route:
+    - destination:
+        host: {{ .Release.Name }}-gerrit-replica-service.{{ .Release.Namespace }}.svc.cluster.local
+        port:
+          number: 80
+  {{ if .Values.istio.ssh.enabled }}
+  tcp:
+  - match:
+    - port: {{ .Values.gerritReplica.service.ssh.port }}
+    route:
+    - destination:
+        host: {{ .Release.Name }}-gerrit-replica-service.{{ .Release.Namespace }}.svc.cluster.local
+        port:
+          number: {{ .Values.gerritReplica.service.ssh.port }}
+  {{- end }}
+---
+apiVersion: networking.istio.io/v1alpha3
+kind: DestinationRule
+metadata:
+  name: {{ .Release.Name }}-gerrit-destination-rule
+  labels:
+    app.kubernetes.io/component: gerrit-replica
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit-replica.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+spec:
+  host: {{ .Release.Name }}-gerrit-replica-service.{{ .Release.Namespace }}.svc.cluster.local
+  trafficPolicy:
+    loadBalancer:
+      simple: LEAST_CONN
+{{- end }}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/log-cleaner.cronjob.yaml b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/log-cleaner.cronjob.yaml
new file mode 100644
index 0000000..cbeb88f
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/log-cleaner.cronjob.yaml
@@ -0,0 +1,69 @@
+{{- if and .Values.logStorage.enabled .Values.logStorage.cleanup.enabled }}
+apiVersion: batch/v1
+kind: CronJob
+metadata:
+  name: {{ .Release.Name }}-log-cleaner
+  labels:
+    app.kubernetes.io/component: log-cleaner
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit-replica.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+spec:
+  schedule: {{ .Values.logStorage.cleanup.schedule | quote }}
+  concurrencyPolicy: "Forbid"
+  jobTemplate:
+    spec:
+      template:
+        metadata:
+          labels:
+            app.kubernetes.io/component: log-cleaner
+            app.kubernetes.io/instance: {{ .Release.Name }}
+            chart: {{ template "gerrit-replica.chart" . }}
+            heritage: {{ .Release.Service }}
+            release: {{ .Release.Name }}
+            {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 12 }}
+            {{- end }}
+            {{- if .Values.logStorage.cleanup.additionalPodLabels }}
+{{ toYaml .Values.logStorage.cleanup.additionalPodLabels  | indent 12 }}
+            {{- end }}
+        {{ if .Values.istio.enabled -}}
+          annotations:
+            sidecar.istio.io/inject: "false"
+        {{- end }}
+        spec:
+          restartPolicy: OnFailure
+          containers:
+          - name: log-cleaner
+            imagePullPolicy: {{ .Values.images.imagePullPolicy }}
+            image: {{ .Values.images.busybox.registry -}}/busybox:{{- .Values.images.busybox.tag }}
+            command:
+            - sh
+            - -c
+            args:
+            - |
+              find /var/logs/ \
+                -mindepth 1 \
+                -type f \
+                -mtime +{{ .Values.logStorage.cleanup.retentionDays }} \
+                -print \
+                -delete
+              find /var/logs/ -type d -empty -delete
+            resources:
+{{ toYaml .Values.logStorage.cleanup.resources | indent 14 }}
+            volumeMounts:
+            - name: logs
+              mountPath: "/var/logs"
+          volumes:
+          - name: logs
+            persistentVolumeClaim:
+              {{- if .Values.logStorage.externalPVC.use }}
+              claimName: {{ .Values.logStorage.externalPVC.name }}
+              {{- else }}
+              claimName: {{ .Release.Name }}-log-pvc
+              {{- end }}
+{{- end }}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/netpol.yaml b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/netpol.yaml
new file mode 100644
index 0000000..72a2bbd
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/netpol.yaml
@@ -0,0 +1,248 @@
+{{ if .Values.networkPolicies.enabled -}}
+kind: NetworkPolicy
+apiVersion: networking.k8s.io/v1
+metadata:
+  name: {{ .Release.Name }}-default-deny-all
+  labels:
+    chart: {{ template "gerrit-replica.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+spec:
+  podSelector:
+    matchLabels:
+      chart: {{ template "gerrit-replica.chart" . }}
+      release: {{ .Release.Name }}
+  policyTypes:
+  - Ingress
+  - Egress
+  ingress: []
+  egress: []
+---
+{{ if .Values.networkPolicies.dnsPorts -}}
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: {{ .Release.Name }}-allow-dns-access
+  labels:
+    chart: {{ template "gerrit-replica.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+spec:
+  podSelector:
+    matchLabels:
+      chart: {{ template "gerrit-replica.chart" . }}
+      release: {{ .Release.Name }}
+  policyTypes:
+  - Egress
+  egress:
+  - ports:
+    {{ range .Values.networkPolicies.dnsPorts -}}
+    - port: {{ . }}
+      protocol: UDP
+    - port: {{ . }}
+      protocol: TCP
+    {{ end }}
+{{- end }}
+---
+kind: NetworkPolicy
+apiVersion: networking.k8s.io/v1
+metadata:
+  name: gerrit-replica-allow-external
+  labels:
+    app.kubernetes.io/component: gerrit-replica
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit-replica.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+spec:
+  podSelector:
+    matchLabels:
+      chart: {{ template "gerrit-replica.chart" . }}
+      release: {{ .Release.Name }}
+      app.kubernetes.io/component: gerrit-replica
+      app.kubernetes.io/instance: {{ .Release.Name }}
+  ingress:
+  - ports:
+    - port: 8080
+    from: []
+---
+{{ if or .Values.gitBackend.networkPolicy.ingress -}}
+kind: NetworkPolicy
+apiVersion: networking.k8s.io/v1
+metadata:
+  name: git-backend-custom-ingress-policies
+  labels:
+    app.kubernetes.io/component: git-backend
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit-replica.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+spec:
+  policyTypes:
+  - Ingress
+  podSelector:
+    matchLabels:
+      chart: {{ template "gerrit-replica.chart" . }}
+      release: {{ .Release.Name }}
+      app.kubernetes.io/component: git-backend
+      app.kubernetes.io/instance: {{ .Release.Name }}
+  ingress:
+{{ toYaml .Values.gitBackend.networkPolicy.ingress | indent 2 }}
+{{- end }}
+---
+{{ if or .Values.gitBackend.networkPolicy.egress -}}
+kind: NetworkPolicy
+apiVersion: networking.k8s.io/v1
+metadata:
+  name: git-backend-custom-egress-policies
+  labels:
+    app.kubernetes.io/component: git-backend
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit-replica.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+spec:
+  policyTypes:
+  - Egress
+  podSelector:
+    matchLabels:
+      chart: {{ template "gerrit-replica.chart" . }}
+      release: {{ .Release.Name }}
+      app.kubernetes.io/component: git-backend
+      app.kubernetes.io/instance: {{ .Release.Name }}
+  egress:
+{{ toYaml .Values.gitBackend.networkPolicy.egress | indent 2 }}
+{{- end }}
+---
+{{ if or .Values.gerritReplica.networkPolicy.ingress -}}
+kind: NetworkPolicy
+apiVersion: networking.k8s.io/v1
+metadata:
+  name: gerrit-replica-custom-ingress-policies
+  labels:
+    app.kubernetes.io/component: gerrit-replica
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit-replica.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+spec:
+  policyTypes:
+  - Ingress
+  podSelector:
+    matchLabels:
+      chart: {{ template "gerrit-replica.chart" . }}
+      release: {{ .Release.Name }}
+      app.kubernetes.io/component: gerrit-replica
+      app.kubernetes.io/instance: {{ .Release.Name }}
+  ingress:
+{{ toYaml .Values.gerritReplica.networkPolicy.ingress | indent 2 }}
+{{- end }}
+---
+{{ if or .Values.gerritReplica.networkPolicy.egress -}}
+kind: NetworkPolicy
+apiVersion: networking.k8s.io/v1
+metadata:
+  name: gerrit-replica-custom-egress-policies
+  labels:
+    app.kubernetes.io/component: gerrit-replica
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit-replica.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+spec:
+  policyTypes:
+  - Egress
+  podSelector:
+    matchLabels:
+      chart: {{ template "gerrit-replica.chart" . }}
+      release: {{ .Release.Name }}
+      app.kubernetes.io/component: gerrit-replica
+      app.kubernetes.io/instance: {{ .Release.Name }}
+  egress:
+{{ toYaml .Values.gerritReplica.networkPolicy.egress | indent 2 }}
+{{- end }}
+---
+{{ if or .Values.istio.enabled -}}
+kind: NetworkPolicy
+apiVersion: networking.k8s.io/v1
+metadata:
+  name: istio-proxy
+  labels:
+    chart: {{ template "gerrit-replica.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+spec:
+  policyTypes:
+  - Egress
+  - Ingress
+  podSelector:
+    matchLabels:
+      chart: {{ template "gerrit-replica.chart" . }}
+      release: {{ .Release.Name }}
+  egress:
+  - ports:
+    - protocol: TCP
+      port: 15012
+  ingress:
+  - ports:
+    - protocol: TCP
+      port: 15012
+---
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: {{ .Release.Name }}-istio-ingress
+  labels:
+    chart: {{ template "gerrit-replica.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+spec:
+  podSelector:
+    matchLabels:
+      chart: {{ template "gerrit-replica.chart" . }}
+      release: {{ .Release.Name }}
+  ingress:
+  - ports:
+    - protocol: TCP
+      port: 80
+    {{ if .Values.istio.ssh.enabled }}
+    - protocol: TCP
+      port: {{ .Values.gerritReplica.service.ssh.port }}
+    {{- end }}
+    from:
+    - namespaceSelector:
+        matchLabels:
+          name: istio-system
+    - podSelector:
+        matchLabels:
+          istio: ingressgateway
+
+{{- end }}
+{{- end }}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/nfs.configmap.yaml b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/nfs.configmap.yaml
new file mode 100644
index 0000000..32b167b
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/nfs.configmap.yaml
@@ -0,0 +1,28 @@
+{{- if and .Values.nfsWorkaround.enabled .Values.nfsWorkaround.idDomain -}}
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: {{ .Release.Name }}-nfs-configmap
+  labels:
+    app.kubernetes.io/component: gerrit-replica
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit-replica.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+data:
+  idmapd.conf: |-
+    [General]
+
+    Verbosity = 0
+    Pipefs-Directory = /run/rpc_pipefs
+    # set your own domain here, if it differs from FQDN minus hostname
+    Domain = {{ .Values.nfsWorkaround.idDomain }}
+
+    [Mapping]
+
+    Nobody-User = nobody
+    Nobody-Group = nogroup
+{{- end }}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/promtail.configmap.yaml b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/promtail.configmap.yaml
new file mode 100644
index 0000000..8dac380
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/promtail.configmap.yaml
@@ -0,0 +1,94 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: {{ .Release.Name }}-promtail-gerrit-configmap
+  labels:
+    app.kubernetes.io/component: gerrit-replica
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit-replica.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+data:
+  promtail.yaml: |-
+    positions:
+      filename: /var/gerrit/logs/promtail-positions.yaml
+
+    client:
+        tls_config:
+          insecure_skip_verify: {{ .Values.promtailSidecar.tls.skipVerify }}
+          {{- if not .Values.promtailSidecar.tls.skipVerify }}
+          ca_file: /etc/promtail/promtail.ca.crt
+          {{- end }}
+        basic_auth:
+          username: {{ .Values.promtailSidecar.loki.user }}
+          password_file: /etc/promtail/promtail.secret
+    scrape_configs:
+    - job_name: gerrit_error
+      static_configs:
+      - targets:
+        - localhost
+        labels:
+          job: gerrit_error
+          __path__: /var/gerrit/logs/error_log.json
+      entry_parser: raw
+      pipeline_stages:
+      - json:
+          expressions:
+            timestamp: '"@timestamp"'
+            message:
+      - template:
+          source: timestamp
+          template: {{`'{{ Replace .Value "," "." 1 }}'`}}
+      - template:
+          source: timestamp
+          template: {{`'{{ Replace .Value "Z" " +0000" 1 }}'`}}
+      - template:
+          source: timestamp
+          template: {{`'{{ Replace .Value "T" " " 1 }}'`}}
+      - timestamp:
+          source: timestamp
+          format: "2006-01-02 15:04:05.999 -0700"
+      - regex:
+          source: message
+          expression: "Gerrit Code Review (?P<gerrit_version>.*) ready"
+      - labels:
+          gerrit_version:
+    - job_name: gerrit_httpd
+      static_configs:
+      - targets:
+        - localhost
+        labels:
+          job: gerrit_httpd
+          __path__: /var/gerrit/logs/httpd_log.json
+      entry_parser: raw
+      pipeline_stages:
+      - json:
+          expressions:
+            timestamp: null
+      - template:
+          source: timestamp
+          template: {{`'{{ Replace .Value "," "." 1 }}'`}}
+      - timestamp:
+          format: 02/Jan/2006:15:04:05.999 -0700
+          source: timestamp
+    - job_name: gerrit_sshd
+      static_configs:
+      - targets:
+        - localhost
+        labels:
+          job: gerrit_sshd
+          __path__: /var/gerrit/logs/sshd_log.json
+      entry_parser: raw
+      pipeline_stages:
+      - json:
+          expressions:
+            timestamp:
+      - template:
+          source: timestamp
+          template: {{`'{{ Replace .Value "," "." 1 }}'`}}
+      - timestamp:
+          source: timestamp
+          format: 2006-01-02 15:04:05.999 -0700
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/promtail.secret.yaml b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/promtail.secret.yaml
new file mode 100644
index 0000000..012fb5b
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/promtail.secret.yaml
@@ -0,0 +1,18 @@
+{{ if .Values.promtailSidecar.enabled -}}
+apiVersion: v1
+kind: Secret
+metadata:
+  name:  {{ .Release.Name }}-promtail-secret
+  labels:
+    app.kubernetes.io/component: gerrit-replica
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit-replica.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+type: Opaque
+data:
+  promtail.secret: {{ .Values.promtailSidecar.loki.password | b64enc }}
+{{- end }}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/storage.pvc.yaml b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/storage.pvc.yaml
new file mode 100644
index 0000000..5f8974e
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/storage.pvc.yaml
@@ -0,0 +1,27 @@
+{{- if not .Values.gitRepositoryStorage.externalPVC.use }}
+kind: PersistentVolumeClaim
+apiVersion: v1
+metadata:
+  name: {{ .Release.Name }}-git-repositories-pvc
+spec:
+  accessModes:
+  - ReadWriteMany
+  resources:
+    requests:
+      storage: {{ .Values.gitRepositoryStorage.size }}
+  storageClassName: {{ .Values.storageClasses.shared.name }}
+{{- end }}
+{{- if and .Values.logStorage.enabled (not .Values.logStorage.externalPVC.use) }}
+---
+kind: PersistentVolumeClaim
+apiVersion: v1
+metadata:
+  name: {{ .Release.Name }}-log-pvc
+spec:
+  accessModes:
+  - ReadWriteMany
+  resources:
+    requests:
+      storage: {{ .Values.logStorage.size }}
+  storageClassName: {{ .Values.storageClasses.shared.name }}
+{{- end }}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/storageclasses.yaml b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/storageclasses.yaml
new file mode 100644
index 0000000..fb91856
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-replica/templates/storageclasses.yaml
@@ -0,0 +1,57 @@
+{{ if .Values.storageClasses.default.create -}}
+kind: StorageClass
+apiVersion: storage.k8s.io/v1
+metadata:
+  name: {{ .Values.storageClasses.default.name }}
+  labels:
+    chart: {{ template "gerrit-replica.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+provisioner: {{ .Values.storageClasses.default.provisioner }}
+reclaimPolicy: {{ .Values.storageClasses.default.reclaimPolicy }}
+{{ if .Values.storageClasses.default.parameters -}}
+parameters:
+{{- range $key, $value := .Values.storageClasses.default.parameters }}
+  {{ $key }}: {{ $value }}
+{{- end }}
+{{ if .Values.storageClasses.default.mountOptions -}}
+mountOptions:
+{{- range .Values.storageClasses.default.mountOptions }}
+  - {{ . }}
+{{- end }}
+{{- end }}
+allowVolumeExpansion: {{ .Values.storageClasses.default.allowVolumeExpansion }}
+{{- end }}
+{{- end }}
+---
+{{ if .Values.storageClasses.shared.create -}}
+kind: StorageClass
+apiVersion: storage.k8s.io/v1
+metadata:
+  name: {{ .Values.storageClasses.shared.name }}
+  labels:
+    chart: {{ template "gerrit-replica.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+provisioner: {{ .Values.storageClasses.shared.provisioner }}
+reclaimPolicy: {{ .Values.storageClasses.shared.reclaimPolicy }}
+{{ if .Values.storageClasses.shared.parameters -}}
+parameters:
+{{- range $key, $value := .Values.storageClasses.shared.parameters }}
+  {{ $key }}: {{ $value }}
+{{- end }}
+{{ if .Values.storageClasses.shared.mountOptions -}}
+mountOptions:
+{{- range .Values.storageClasses.shared.mountOptions }}
+  - {{ . }}
+{{- end }}
+{{- end }}
+allowVolumeExpansion: {{ .Values.storageClasses.shared.allowVolumeExpansion }}
+{{- end }}
+{{- end }}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit-replica/values.yaml b/charts/k8s-gerrit/helm-charts/gerrit-replica/values.yaml
new file mode 100644
index 0000000..3f318f8
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit-replica/values.yaml
@@ -0,0 +1,430 @@
+images:
+  busybox:
+    registry: docker.io
+    tag: latest
+  # Registry used for container images created by this project
+  registry:
+    # The registry name must NOT contain a trailing slash
+    name:
+    ImagePullSecret:
+      # Leave blank, if no ImagePullSecret is needed.
+      name: image-pull-secret
+      # If set to false, the gerrit-replica chart expects either a ImagePullSecret
+      # with the name configured above to be present on the cluster or that no
+      # credentials are needed.
+      create: false
+      username:
+      password:
+  version: latest
+  imagePullPolicy: Always
+  # Additional ImagePullSecrets that already exist and should be used by the
+  # pods of this chart. E.g. to pull busybox from dockerhub.
+  additionalImagePullSecrets: []
+
+# Additional labels that should be applied to all resources
+additionalLabels: {}
+
+storageClasses:
+  # Storage class used for storing logs and other pod-specific persisted data
+  default:
+    # If create is set to false, an existing StorageClass with the given
+    # name is expected to exist in the cluster. Setting create to true will
+    # create a storage class with the parameters given below.
+    name: default
+    create: false
+    provisioner: kubernetes.io/aws-ebs
+    reclaimPolicy: Delete
+    # Use the parameters key to set all parameters needed for the provisioner
+    parameters:
+      type: gp2
+      fsType: ext4
+    mountOptions: []
+    allowVolumeExpansion: false
+  # Storage class used for storing git repositories. Has to provide RWM access.
+  shared:
+    # If create is set to false, an existing StorageClass with RWM access
+    # mode and the given name has to be provided.
+    name: shared-storage
+    create: false
+    provisioner: nfs
+    reclaimPolicy: Delete
+    # Use the parameters key to set all parameters needed for the provisioner
+    parameters:
+      mountOptions: vers=4.1
+    mountOptions: []
+    allowVolumeExpansion: false
+
+nfsWorkaround:
+  enabled: false
+  chownOnStartup: false
+  idDomain: localdomain.com
+
+
+networkPolicies:
+  enabled: false
+  dnsPorts:
+  - 53
+  - 8053
+
+
+gitRepositoryStorage:
+  externalPVC:
+    use: false
+    name: git-repositories-pvc
+  size: 5Gi
+
+
+logStorage:
+  enabled: false
+  externalPVC:
+    use: false
+    name: gerrit-logs-pvc
+  size: 5Gi
+  cleanup:
+    enabled: false
+    additionalPodLabels: {}
+    schedule: "0 0 * * *"
+    retentionDays: 14
+    resources:
+      requests:
+        cpu: 100m
+        memory: 256Mi
+      limits:
+        cpu: 100m
+        memory: 256Mi
+
+
+istio:
+  enabled: false
+  host:
+  tls:
+    enabled: false
+    secret:
+      # If using an external secret, make sure to name the keys `tls.crt`
+      # and `tls.key`, respectively.
+      create: true
+      # `name` will only be used, if `create` is set to false to bind an
+      # existing secret. Otherwise the name will be automatically generated to
+      # avoid conflicts between multiple chart installations.
+      name:
+    # `cert`and `key` will only be used, if the secret will be created by
+    # this chart.
+    cert: |-
+      -----BEGIN CERTIFICATE-----
+
+      -----END CERTIFICATE-----
+    key: |-
+      -----BEGIN RSA PRIVATE KEY-----
+
+      -----END RSA PRIVATE KEY-----
+  ssh:
+    enabled: false
+
+caCert:
+
+ingress:
+  enabled: false
+  host:
+  # The maximum body size to allow for requests. Use "0" to allow unlimited
+  # reuqest body sizes.
+  maxBodySize: 50m
+  additionalAnnotations:
+    kubernetes.io/ingress.class: nginx
+  #  nginx.ingress.kubernetes.io/server-alias: example.com
+  #  nginx.ingress.kubernetes.io/whitelist-source-range: xxx.xxx.xxx.xxx
+  tls:
+    enabled: false
+    secret:
+      # If using an external secret, make sure to name the keys `tls.crt`
+      # and `tls.key`, respectively.
+      create: true
+      # `name` will only be used, if `create` is set to false to bind an
+      # existing secret. Otherwise the name will be automatically generated to
+      # avoid conflicts between multiple chart installations.
+      name:
+    # `cert`and `key` will only be used, if the secret will be created by
+    # this chart.
+    cert: |-
+      -----BEGIN CERTIFICATE-----
+
+      -----END CERTIFICATE-----
+    key: |-
+      -----BEGIN RSA PRIVATE KEY-----
+
+      -----END RSA PRIVATE KEY-----
+
+promtailSidecar:
+  enabled: false
+  image: grafana/promtail
+  version: 1.3.0
+  resources:
+    requests:
+      cpu: 100m
+      memory: 128Mi
+    limits:
+      cpu: 200m
+      memory: 128Mi
+  tls:
+    skipVerify: true
+  loki:
+    url: loki.example.com
+    user: admin
+    password: secret
+
+
+gitBackend:
+  image: k8sgerrit/apache-git-http-backend
+
+  additionalPodLabels: {}
+  tolerations: []
+  topologySpreadConstraints: {}
+  nodeSelector: {}
+  affinity:
+    podAntiAffinity:
+      preferredDuringSchedulingIgnoredDuringExecution:
+      - weight: 100
+        podAffinityTerm:
+          labelSelector:
+            matchExpressions:
+            - key: app
+              operator: In
+              values:
+              - git-backend
+          topologyKey: "topology.kubernetes.io/zone"
+
+  replicas: 1
+  maxSurge: 25%
+  # For just one replica, 100 % unavailability has to be allowed for updates to
+  # work.
+  maxUnavailable: 100%
+
+  # The general NetworkPolicy rules implemented by this chart may be too restrictive
+  # for some setups. Here custom rules may be added to whitelist some additional
+  # connections.
+  networkPolicy:
+    # This allows ingress traffic from all sources. If possible, this should be
+    # limited to the respective primary Gerrit that replicates to this replica.
+    ingress:
+    - {}
+    egress: []
+
+  resources:
+    requests:
+      cpu: 100m
+      memory: 256Mi
+    limits:
+      cpu: 100m
+      memory: 256Mi
+
+  livenessProbe:
+    initialDelaySeconds: 10
+    periodSeconds: 5
+
+  readinessProbe:
+    initialDelaySeconds: 5
+    periodSeconds: 1
+
+  service:
+    additionalAnnotations: {}
+    loadBalancerSourceRanges: []
+    type: NodePort
+    externalTrafficPolicy: Cluster
+    http:
+      port: 80
+
+  credentials:
+    # example: user: 'git'; password: 'secret'
+    # run `man htpasswd` to learn about how to create .htpasswd-files
+    htpasswd: git:$apr1$O/LbLKC7$Q60GWE7OcqSEMSfe/K8xU.
+    # TODO: Create htpasswd-file on container startup instead and set user
+    # and password in values.yaml.
+    #user:
+    #password:
+
+
+gitGC:
+  image: k8sgerrit/git-gc
+
+  tolerations: []
+  nodeSelector: {}
+  affinity: {}
+  additionalPodLabels: {}
+
+  schedule: 0 6,18 * * *
+
+  resources:
+    requests:
+      cpu: 100m
+      memory: 256Mi
+    limits:
+      cpu: 100m
+      memory: 256Mi
+
+gerritReplica:
+  images:
+    gerritInit: k8sgerrit/gerrit-init
+    gerritReplica: k8sgerrit/gerrit
+
+  tolerations: []
+  topologySpreadConstraints: {}
+  nodeSelector: {}
+  affinity:
+    podAntiAffinity:
+      preferredDuringSchedulingIgnoredDuringExecution:
+      - weight: 100
+        podAffinityTerm:
+          labelSelector:
+            matchExpressions:
+            - key: app
+              operator: In
+              values:
+              - gerrit-replica
+          topologyKey: "topology.kubernetes.io/zone"
+
+  replicas: 1
+  updatePartition: 0
+  additionalAnnotations: {}
+  additionalPodLabels: {}
+
+  livenessProbe:
+    initialDelaySeconds: 60
+    periodSeconds: 5
+
+  readinessProbe:
+    initialDelaySeconds: 10
+    periodSeconds: 10
+
+  startupProbe:
+    initialDelaySeconds: 10
+    periodSeconds: 30
+
+  gracefulStopTimeout: 90
+
+  # The memory limit has to be higher than the configures heap-size for Java!
+  resources:
+    requests:
+      cpu: 1
+      memory: 5Gi
+    limits:
+      cpu: 1
+      memory: 6Gi
+
+  persistence:
+    enabled: true
+    size: 5Gi
+
+  # The general NetworkPolicy rules implemented by this chart may be too restrictive
+  # for some setups, e.g. when trying to connect to an external database. Here
+  # custom rules may be added to whitelist some additional connections.
+  networkPolicy:
+    ingress: []
+    egress: []
+
+  service:
+    additionalAnnotations: {}
+    loadBalancerSourceRanges: []
+    type: NodePort
+    externalTrafficPolicy: Cluster
+    http:
+      port: 80
+    ssh:
+      enabled: false
+      port: 29418
+
+  # `gerritReplica.keystore` expects a base64-encoded Java-keystore
+  # Since Java keystores are binary files, adding the unencoded content and
+  # automatic encoding using helm does not work here.
+  keystore:
+
+  pluginManagement:
+    plugins: []
+    # A plugin packaged in the gerrit.war-file
+    # - name: download-commands
+
+    # A plugin packaged in the gerrit.war-file that will also be installed as a
+    # lib
+    # - name: replication
+    #   installAsLibrary: true
+
+    # A plugin that will be downloaded on startup
+    # - name: delete-project
+    #   url: https://example.com/gerrit-plugins/delete-project.jar
+    #   sha1:
+    #   installAsLibrary: false
+
+    # Only downloaded plugins will be cached. This will be ignored, if no plugins
+    # are downloaded.
+    libs: []
+    cache:
+      enabled: false
+      size: 1Gi
+
+  priorityClassName:
+
+  etc:
+    # Some values are expected to have a specific value for the deployment installed
+    # by this chart to work. These are marked with `# FIXED`.
+    # Do not change them!
+    config:
+      gerrit.config: |-
+        [gerrit]
+          basePath = git # FIXED
+          serverId = gerrit-replica-1
+          # The canonical web URL has to be set to the Ingress host, if an Ingress
+          # is used. If a LoadBalancer-service is used, this should be set to the
+          # LoadBalancer's external IP. This can only be done manually after installing
+          # the chart, when you know the external IP the LoadBalancer got from the
+          # cluster.
+          canonicalWebUrl = http://example.com/
+          disableReverseDnsLookup = true
+        [index]
+          type = LUCENE
+        [index "scheduledIndexer"]
+          runOnStartup = false
+        [auth]
+          type = DEVELOPMENT_BECOME_ANY_ACCOUNT
+        [httpd]
+          # If using an ingress use proxy-http or proxy-https
+          listenUrl = proxy-http://*:8080/
+          requestLog = true
+          gracefulStopTimeout = 1m
+        [sshd]
+          listenAddress = *:29418
+          gracefulStopTimeout = 1m
+        [transfer]
+          timeout = 120 s
+        [user]
+          name = Gerrit Code Review
+          email = gerrit@example.com
+          anonymousCoward = Unnamed User
+        [cache]
+          directory = cache
+        [container]
+          user = gerrit # FIXED
+          replica = true # FIXED
+          javaHome = /usr/lib/jvm/java-11-openjdk # FIXED
+          javaOptions = -Djavax.net.ssl.trustStore=/var/gerrit/etc/keystore # FIXED
+          javaOptions = -Xms200m
+          # Has to be lower than 'gerritReplica.resources.limits.memory'. Also
+          # consider memories used by other applications in the container.
+          javaOptions = -Xmx4g
+
+    secret:
+      secure.config: |-
+        # Password for the keystore added as value for 'gerritReplica.keystore'
+        # Only needed, if SSL is enabled.
+        #[httpd]
+        #  sslKeyPassword = gerrit
+
+      # ssh_host_ecdsa_key: |-
+      #   -----BEGIN EC PRIVATE KEY-----
+
+      #   -----END EC PRIVATE KEY-----
+
+      # ssh_host_ecdsa_key.pub: ecdsa-sha2-nistp256...
+
+  additionalConfigMaps:
+    # - name:
+    #   subDir:
+    #   data:
+    #     file.txt: test
diff --git a/charts/k8s-gerrit/helm-charts/gerrit/.helmignore b/charts/k8s-gerrit/helm-charts/gerrit/.helmignore
new file mode 100644
index 0000000..4f4562f
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit/.helmignore
@@ -0,0 +1,24 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
+
+docs/
+supplements/
diff --git a/charts/k8s-gerrit/helm-charts/gerrit/Chart.yaml b/charts/k8s-gerrit/helm-charts/gerrit/Chart.yaml
new file mode 100644
index 0000000..d41771f
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit/Chart.yaml
@@ -0,0 +1,27 @@
+apiVersion: v2
+appVersion: 3.8.0
+description: |-
+    Gerrit is a free, web-based team code collaboration tool. Software developers
+    in a team can review each other's modifications on their source code using
+    a Web browser and approve or reject those changes. It integrates closely with
+    Git, a distributed version control system. [1]
+
+    [1](https://en.wikipedia.org/wiki/Gerrit_(software)
+name: gerrit
+version: 0.2.0
+maintainers:
+- name: Thomas Draebing
+  email: thomas.draebing@sap.com
+- name: Matthias Sohn
+  email: matthias.sohn@sap.com
+- name: Sasa Zivkov
+  email: sasa.zivkov@sap.com
+- name: Christian Halstrick
+  email: christian.halstrick@sap.com
+home: https://gerrit.googlesource.com/k8s-gerrit/+/master/helm-charts/gerrit-replica
+icon: http://commondatastorage.googleapis.com/gerrit-static/diffy-w200.png
+sources:
+- https://gerrit.googlesource.com/k8s-gerrit/+/master/
+keywords:
+- gerrit
+- git
diff --git a/charts/k8s-gerrit/helm-charts/gerrit/LICENSE b/charts/k8s-gerrit/helm-charts/gerrit/LICENSE
new file mode 100644
index 0000000..028fc9f
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit/LICENSE
@@ -0,0 +1,201 @@
+   Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "{}"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright (C) 2018 The Android Open Source Project
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/charts/k8s-gerrit/helm-charts/gerrit/README.md b/charts/k8s-gerrit/helm-charts/gerrit/README.md
new file mode 100644
index 0000000..110383a
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit/README.md
@@ -0,0 +1,459 @@
+# Gerrit on Kubernetes
+
+Gerrit is a web-based code review tool, which acts as a Git server. This helm
+chart provides a Gerrit setup that can be deployed on Kubernetes.
+In addition, the chart provides a CronJob to perform Git garbage collection.
+
+***note
+Gerrit versions before 3.0 are no longer supported, since the support of ReviewDB
+was removed.
+***
+
+## Prerequisites
+
+- Helm (>= version 3.0)
+
+    (Check out [this guide](https://docs.helm.sh/using_helm/#quickstart-guide)
+    how to install and use helm.)
+
+- Access to a provisioner for persistent volumes with `Read Write Many (RWM)`-
+  capability.
+
+    A list of applicaple volume types can be found
+    [here](https://kubernetes.io/docs/concepts/storage/persistent-volumes/#access-modes).
+    This project was developed using the
+    [NFS-server-provisioner helm chart](https://github.com/helm/charts/tree/master/stable/nfs-server-provisioner),
+    a NFS-provisioner deployed in the Kubernetes cluster itself. Refer to
+    [this guide](/helm-charts/gerrit/docs/nfs-provisioner.md) of how to
+    deploy it in context of this project.
+
+- A domain name that is configured to point to the IP address of the node running
+  the Ingress controller on the kubernetes cluster (as described
+  [here](http://alesnosek.com/blog/2017/02/14/accessing-kubernetes-pods-from-outside-of-the-cluster/)).
+
+- (Optional: Required, if SSL is configured)
+  A [Java keystore](https://gerrit-review.googlesource.com/Documentation/config-gerrit.html#httpd.sslKeyStore)
+  to be used by Gerrit.
+
+## Installing the Chart
+
+***note
+**ATTENTION:** The value for `ingress.host` is required for rendering
+the chart's templates. The nature of the value does not allow defaults.
+Thus a custom `values.yaml`-file setting this value is required!
+***
+
+To install the chart with the release name `gerrit`, execute:
+
+```sh
+cd $(git rev-parse --show-toplevel)/helm-charts
+helm install \
+  gerrit \  # release name
+  ./gerrit \  # path to chart
+  -f <path-to-custom-values>.yaml
+```
+
+The command deploys the Gerrit instance on the current Kubernetes cluster.
+The [configuration section](#Configuration) lists the parameters that can be
+configured during installation.
+
+## Configuration
+
+The following sections list the configurable values in `values.yaml`. To configure
+a Gerrit setup, make a copy of the `values.yaml`-file and change the parameters
+as needed. The configuration can be applied by installing the chart as described
+[above](#Installing-the-chart).
+
+In addition, single options can be set without creating a custom `values.yaml`:
+
+```sh
+cd $(git rev-parse --show-toplevel)/helm-charts
+helm install \
+  gerrit \  # release name
+  ./gerrit \  # path to chart
+  --set=gitRepositoryStorage.size=100Gi
+```
+
+### Container images
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `images.busybox.registry` | The registry to pull the busybox container images from | `docker.io` |
+| `images.busybox.tag` | The busybox image tag to use | `latest` |
+| `images.registry.name` | The image registry to pull the container images from | `` |
+| `images.registry.ImagePullSecret.name` | Name of the ImagePullSecret | `image-pull-secret` (if empty no image pull secret will be deployed) |
+| `images.registry.ImagePullSecret.create` | Whether to create an ImagePullSecret | `false` |
+| `images.registry.ImagePullSecret.username` | The image registry username | `nil` |
+| `images.registry.ImagePullSecret.password` | The image registry password | `nil` |
+| `images.version` | The image version (image tag) to use | `latest` |
+| `images.imagePullPolicy` | Image pull policy | `Always` |
+| `images.additionalImagePullSecrets` | Additional image pull policies that pods should use | `[]` |
+
+### Labels
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `additionalLabels` | Additional labels for resources managed by this Helm chart | `{}` |
+
+### Storage classes
+
+For information of how a `StorageClass` is configured in Kubernetes, read the
+[official Documentation](https://kubernetes.io/docs/concepts/storage/storage-classes/#introduction).
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `storageClasses.default.name` | The name of the default StorageClass (RWO) | `default` |
+| `storageClasses.default.create` | Whether to create the StorageClass | `false` |
+| `storageClasses.default.provisioner` | Provisioner of the StorageClass | `kubernetes.io/aws-ebs` |
+| `storageClasses.default.reclaimPolicy` | Whether to `Retain` or `Delete` volumes, when they become unbound | `Delete` |
+| `storageClasses.default.parameters` | Parameters for the provisioner | `parameters.type: gp2`, `parameters.fsType: ext4` |
+| `storageClasses.default.mountOptions` | The mount options of the default StorageClass | `[]` |
+| `storageClasses.default.allowVolumeExpansion` | Whether to allow volume expansion. | `false` |
+| `storageClasses.shared.name` | The name of the shared StorageClass (RWM) | `shared-storage` |
+| `storageClasses.shared.create` | Whether to create the StorageClass | `false` |
+| `storageClasses.shared.provisioner` | Provisioner of the StorageClass | `nfs` |
+| `storageClasses.shared.reclaimPolicy` | Whether to `Retain` or `Delete` volumes, when they become unbound | `Delete` |
+| `storageClasses.shared.parameters` | Parameters for the provisioner | `parameters.mountOptions: vers=4.1` |
+| `storageClasses.shared.mountOptions` | The mount options of the shared StorageClass | `[]` |
+| `storageClasses.shared.allowVolumeExpansion` | Whether to allow volume expansion. | `false` |
+
+### Network policies
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `networkPolicies.enabled` | Whether to enable preconfigured NetworkPolicies | `false` |
+| `networkPolicies.dnsPorts` | List of ports used by DNS-service (e.g. KubeDNS) | `[53, 8053]` |
+
+The NetworkPolicies provided here are quite strict and do not account for all
+possible scenarios. Thus, custom NetworkPolicies have to be added, e.g. for
+allowing Gerrit to replicate to a Gerrit replica. By default, the egress traffic
+of the gerrit pod is blocked, except for connections to the DNS-server.
+Thus, replication which requires Gerrit to perform git pushes to the replica will
+not work. The chart provides the possibility to define custom rules for egress-
+traffic of the gerrit pod under `gerrit.networkPolicy.egress`.
+Depending on the scenario, there are different ways to allow the required
+connections. The easiest way is to allow all egress-traffic for the gerrit
+pods:
+
+```yaml
+gerrit:
+  networkPolicy:
+    egress:
+    - {}
+```
+
+If the remote that is replicated to is running in a pod on the same cluster and
+the service-DNS is used as the remote's URL (e.g. http://gerrit-replica-git-backend-service:80/git/${name}.git),
+a podSelector (and namespaceSelector, if the pod is running in a different
+namespace) can be used to whitelist the traffic:
+
+```yaml
+gerrit:
+  networkPolicy:
+    egress:
+    - to:
+      - podSelector:
+          matchLabels:
+            app: git-backend
+```
+
+If the remote is outside the cluster, the IP of the remote or its load balancer
+can also be whitelisted, e.g.:
+
+```yaml
+gerrit:
+  networkPolicy:
+    egress:
+    - to:
+      - ipBlock:
+          cidr: xxx.xxx.0.0/16
+```
+
+The same principle also applies to other use cases, e.g. connecting to a database.
+For more information about the NetworkPolicy resource refer to the
+[Kubernetes documentation](https://kubernetes.io/docs/concepts/services-networking/network-policies/).
+
+### Workaround for NFS
+
+Kubernetes will not always be able to adapt the ownership of the files within NFS
+volumes. Thus, a workaround exists that will add init-containers to
+adapt file ownership. Note, that only the ownership of the root directory of the
+volume will be changed. All data contained within will be expected to already be
+owned by the user used by Gerrit. Also the ID-domain will be configured to ensure
+correct ID-mapping.
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `nfsWorkaround.enabled` | Whether the volume used is an NFS-volume | `false` |
+| `nfsWorkaround.chownOnStartup` | Whether to chown the volume on pod startup | `false` |
+| `nfsWorkaround.idDomain` | The ID-domain that should be used to map user-/group-IDs for the NFS mount | `localdomain.com` |
+
+### Storage for Git repositories
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `gitRepositoryStorage.externalPVC.use` | Whether to use a PVC deployed outside the chart | `false` |
+| `gitRepositoryStorage.externalPVC.name` | Name of the external PVC | `git-repositories-pvc` |
+| `gitRepositoryStorage.size` | Size of the volume storing the Git repositories | `5Gi` |
+
+If the git repositories should be persisted even if the chart is deleted and in
+a way that the volume containing them can be mounted by the reinstalled chart,
+the PVC claiming the volume has to be created independently of the chart. To use
+the external PVC, set `gitRepositoryStorage.externalPVC.enabled` to `true` and
+give the name of the PVC under `gitRepositoryStorage.externalPVC.name`.
+
+### Storage for Logs
+
+The logs can be stored in a dedicated persistent volume. This volume has to be a
+read-write-many volume to be able to be used by multiple pods.
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `logStorage.enabled` | Whether to enable persistence of logs | `false` |
+| `logStorage.externalPVC.use` | Whether to use a PVC deployed outside the chart | `false` |
+| `logStorage.externalPVC.name` | Name of the external PVC | `gerrit-logs-pvc` |
+| `logStorage.size` | Size of the volume | `5Gi` |
+| `logStorage.cleanup.enabled` | Whether to regularly delete old logs | `false` |
+| `logStorage.cleanup.schedule` | Cron schedule defining when to run the cleanup job | `0 0 * * *` |
+| `logStorage.cleanup.retentionDays` | Number of days to retain the logs | `14` |
+| `logStorage.cleanup.resources` | Resources the container is allowed to use | `requests.cpu: 100m` |
+| `logStorage.cleanup.additionalPodLabels` | Additional labels for pods | `{}` |
+| | | `requests.memory: 256Mi` |
+| | | `limits.cpu: 100m` |
+| | | `limits.memory: 256Mi` |
+
+Each pod will create a separate folder for its logs, allowing to trace logs to
+the respective pods.
+
+### CA certificate
+
+Some application may require TLS verification. If the default CA built into the
+containers is not enough a custom CA certificate can be given to the deployment.
+Note, that Gerrit will require its CA in a JKS keytore, which is described below.
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `caCert` | CA certificate for TLS verification (if not set, the default will be used) | `None` |
+
+### Ingress
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `ingress.enabled` | Whether to enable the Ingress | `false` |
+| `ingress.host` | REQUIRED: Host name to use for the Ingress (required for Ingress) | `nil` |
+| `ingress.additionalAnnotations` | Additional annotations for the Ingress | `nil` |
+| `ingress.tls.enabled` | Whether to enable TLS termination in the Ingress | `false` |
+| `ingress.tls.secret.create` | Whether to create a TLS-secret | `true` |
+| `ingress.tls.secret.name` | Name of an external secret that will be used as a TLS-secret | `nil` |
+| `ingress.tls.cert` | Public SSL server certificate | `-----BEGIN CERTIFICATE-----` |
+| `ingress.tls.key` | Private SSL server certificate | `-----BEGIN RSA PRIVATE KEY-----` |
+
+***note
+For graceful shutdown to work with an ingress, the ingress controller has to be
+configured to gracefully close the connections as well.
+***
+
+### Git garbage collection
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `gitGC.image` | Image name of the Git-GC container image | `k8sgerrit/git-gc` |
+| `gitGC.schedule` | Cron-formatted schedule with which to run Git garbage collection | `0 6,18 * * *` |
+| `gitGC.resources` | Configure the amount of resources the pod requests/is allowed | `requests.cpu: 100m` |
+|                   |                                                               | `requests.memory: 256Mi` |
+|                   |                                                               | `limits.cpu: 100m` |
+|                   |                                                               | `limits.memory: 256Mi` |
+| `gitGC.logging.persistence.enabled` | Whether to persist logs | `true` |
+| `gitGC.logging.persistence.size` | Storage size for persisted logs | `1Gi` |
+| `gitGC.tolerations` | Taints and tolerations work together to ensure that pods are not scheduled onto inappropriate nodes. For more information, please refer to the following documents. [Taints and Tolerations](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration) | [] |
+| `gitGC.nodeSelector` | Assigns a Pod to the specified Nodes. For more information, please refer to the following documents. [Assign Pods to Nodes](https://kubernetes.io/docs/tasks/configure-pod-container/assign-pods-nodes/). [Assigning Pods to Nodes](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/) | {} |
+| `gitGC.affinity` | Assigns a Pod to the specified Nodes. For more information, please refer to the following documents. [Assign Pods to Nodes using Node Affinity](https://kubernetes.io/docs/tasks/configure-pod-container/assign-pods-nodes-using-node-affinity/). [Assigning Pods to Nodes](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/) | {} |
+| `gitGC.additionalPodLabels` | Additional labels for Pods | `{}` |
+
+### Gerrit
+
+***note
+The way the Jetty servlet used by Gerrit works, the Gerrit component of the
+gerrit chart actually requires the URL to be known, when the chart is installed.
+The suggested way to do that is to use the provided Ingress resource. This requires
+that a URL is available and that the DNS is configured to point the URL to the
+IP of the node the Ingress controller is running on!
+***
+
+***note
+Setting the canonical web URL in the gerrit.config to the host used for the Ingress
+is mandatory, if access to Gerrit is required!
+***
+
+***note
+While the chart allows to configure multiple replica for the Gerrit StatefulSet,
+scaling of Gerrit is currently not supported, since no mechanism to guarantee a
+consistent state is currently in place. This is planned to be implemented in the
+future.
+***
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `gerrit.images.gerritInit` | Image name of the Gerrit init container image | `k8sgerrit/gerrit-init` |
+| `gerrit.images.gerrit` | Image name of the Gerrit container image | `k8sgerrit/gerrit` |
+| `gerrit.tolerations` | Taints and tolerations work together to ensure that pods are not scheduled onto inappropriate nodes. For more information, please refer to the following documents. [Taints and Tolerations](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration) | [] |
+| `gerrit.topologySpreadConstraints` | Control how Pods are spread across your cluster among failure-domains. For more information, please refer to the following documents. [Pod Topology Spread Constraints](https://kubernetes.io/docs/concepts/workloads/pods/pod-topology-spread-constraints) | {} |
+| `gerrit.nodeSelector` | Assigns a Pod to the specified Nodes. For more information, please refer to the following documents. [Assign Pods to Nodes](https://kubernetes.io/docs/tasks/configure-pod-container/assign-pods-nodes/). [Assigning Pods to Nodes](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/) | {} |
+| `gerrit.affinity` | Assigns a Pod to the specified Nodes. For more information, please refer to the following documents. [Assign Pods to Nodes using Node Affinity](https://kubernetes.io/docs/tasks/configure-pod-container/assign-pods-nodes-using-node-affinity/). [Assigning Pods to Nodes](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/) | {} |
+| `gerrit.additionalAnnotations` | Additional annotations for the Pods | {} |
+| `gerrit.additionalPodLabels` | Additional labels for Pods | `{}` |
+| `gerrit.replicas` | Number of replica pods to deploy | `1` |
+| `gerrit.updatePartition` | Ordinal at which to start updating pods. Pods with a lower ordinal will not be updated. | `0` |
+| `gerrit.resources` | Configure the amount of resources the pod requests/is allowed | `requests.cpu: 1` |
+|                    |                                                               | `requests.memory: 5Gi` |
+|                    |                                                               | `limits.cpu: 1` |
+|                    |                                                               | `limits.memory: 6Gi` |
+| `gerrit.persistence.enabled` | Whether to persist the Gerrit site | `true` |
+| `gerrit.persistence.size` | Storage size for persisted Gerrit site | `10Gi` |
+| `gerrit.livenessProbe` | Configuration of the liveness probe timings | `{initialDelaySeconds: 30, periodSeconds: 5}` |
+| `gerrit.readinessProbe` | Configuration of the readiness probe timings | `{initialDelaySeconds: 5, periodSeconds: 1}` |
+| `gerrit.startupProbe` | Configuration of the startup probe timings | `{initialDelaySeconds: 10, periodSeconds: 5}` |
+| `gerrit.gracefulStopTimeout` | Time in seconds Kubernetes will wait until killing the pod during termination (has to be longer then Gerrit's httpd.gracefulStopTimeout to allow graceful shutdown of Gerrit) | `90` |
+| `gerrit.networkPolicy.ingress` | Custom ingress-network policy for gerrit pods | `nil` |
+| `gerrit.networkPolicy.egress` | Custom egress-network policy for gerrit pods | `nil` |
+| `gerrit.service.additionalAnnotations` | Additional annotations for the Service | `{}` |
+| `gerrit.service.loadBalancerSourceRanges` | The list of allowed IPs for the Service | `[]` |
+| `gerrit.service.type` | Which kind of Service to deploy | `NodePort` |
+| `gerrit.service.externalTrafficPolicy` | Specify how traffic from external is handled | `Cluster` |
+| `gerrit.service.http.port` | Port over which to expose HTTP | `80` |
+| `gerrit.service.ssh.enabled` | Whether to enable SSH | `false` |
+| `gerrit.service.ssh.port` | Port over which to expose SSH | `29418` |
+| `gerrit.keystore` | base64-encoded Java keystore (`cat keystore.jks \| base64`) to be used by Gerrit, when using SSL | `nil` |
+| `gerrit.index.type` | Index type used by Gerrit (either `lucene` or `elasticsearch`) | `lucene` |
+| `gerrit.pluginManagement.plugins` | List of Gerrit plugins to install | `[]` |
+| `gerrit.pluginManagement.plugins[0].name` | Name of plugin | `nil` |
+| `gerrit.pluginManagement.plugins[0].url` | Download url of plugin. If given the plugin will be downloaded, otherwise it will be installed from the gerrit.war-file. | `nil` |
+| `gerrit.pluginManagement.plugins[0].sha1` | SHA1 sum of plugin jar used to ensure file integrity and version (optional) | `nil` |
+| `gerrit.pluginManagement.plugins[0].installAsLibrary` | Whether the plugin should be symlinked to the lib-dir in the Gerrit site. | `nil` |
+| `gerrit.pluginManagement.libs` | List of Gerrit library modules to install | `[]` |
+| `gerrit.pluginManagement.libs[0].name` | Name of the lib module | `nil` |
+| `gerrit.pluginManagement.libs[0].url` | Download url of lib module. | `nil` |
+| `gerrit.pluginManagement.libs[0].sha1` | SHA1 sum of plugin jar used to ensure file integrity and version | `nil` |
+| `gerrit.pluginManagement.cache.enabled` | Whether to cache downloaded plugins | `false` |
+| `gerrit.pluginManagement.cache.size` | Size of the volume used to store cached plugins | `1Gi` |
+| `gerrit.priorityClassName` | Name of the PriorityClass to apply to the master pod | `nil` |
+| `gerrit.etc.config` | Map of config files (e.g. `gerrit.config`) that will be mounted to `$GERRIT_SITE/etc`by a ConfigMap | `{gerrit.config: ..., replication.config: ...}`[see here](#Gerrit-config-files) |
+| `gerrit.etc.secret` | Map of config files (e.g. `secure.config`) that will be mounted to `$GERRIT_SITE/etc`by a Secret | `{secure.config: ...}` [see here](#Gerrit-config-files) |
+| `gerrit.additionalConfigMaps` | Allows to mount additional ConfigMaps into a subdirectory of `$SITE/data` | `[]` |
+| `gerrit.additionalConfigMaps[*].name` | Name of the ConfigMap | `nil` |
+| `gerrit.additionalConfigMaps[*].subDir` | Subdirectory under `$SITE/data` into which the files should be symlinked | `nil` |
+| `gerrit.additionalConfigMaps[*].data` | Data of the ConfigMap. If not set, ConfigMap has to be created manually | `nil` |
+
+### Gerrit config files
+
+The gerrit chart provides a ConfigMap containing the configuration files
+used by Gerrit, e.g. `gerrit.config` and a Secret containing sensitive configuration
+like the `secure.config` to configure the Gerrit installation in the Gerrit
+component. The content of the config files can be set in the `values.yaml` under
+the keys `gerrit.etc.config` and `gerrit.etc.secret` respectively.
+The key has to be the filename (eg. `gerrit.config`) and the file's contents
+the value. This way an arbitrary number of configuration files can be loaded into
+the `$GERRIT_SITE/etc`-directory, e.g. for plugins.
+All configuration options for Gerrit are described in detail in the
+[official documentation of Gerrit](https://gerrit-review.googlesource.com/Documentation/config-gerrit.html).
+Some options however have to be set in a specified way for Gerrit to work as
+intended with the chart:
+
+- `gerrit.basePath`
+
+    Path to the directory containing the repositories. The chart mounts this
+    directory from a persistent volume to `/var/gerrit/git` in the container. For
+    Gerrit to find the correct directory, this has to be set to `git`.
+
+- `gerrit.serverId`
+
+    In Gerrit-version higher than 2.14 Gerrit needs a server ID, which is used by
+    NoteDB. Gerrit would usually generate a random ID on startup, but since the
+    gerrit.config file is read only, when mounted as a ConfigMap this fails.
+    Thus the server ID has to be set manually!
+
+- `gerrit.canonicalWebUrl`
+
+    The canonical web URL has to be set to the Ingress host.
+
+- `httpd.listenURL`
+
+    This has to be set to `proxy-http://*:8080/` or `proxy-https://*:8080`,
+    depending of TLS is enabled in the Ingress or not, otherwise the Jetty
+    servlet will run into an endless redirect loop.
+
+- `httpd.gracefulStopTimeout` / `sshd.gracefulStopTimeout`
+
+    To enable graceful shutdown of the embedded jetty server and SSHD, a timeout
+    has to be set with this option. This will be the maximum time, Gerrit will wait
+    for HTTP requests to finish before shutdown.
+
+- `container.user`
+
+    The technical user in the Gerrit container is called `gerrit`. Thus, this
+    value is required to be `gerrit`.
+
+- `container.javaHome`
+
+    This has to be set to `/usr/lib/jvm/java-11-openjdk-amd64`, since this is
+    the path of the Java installation in the container.
+
+- `container.javaOptions`
+
+    The maximum heap size has to be set. And its value has to be lower than the
+    memory resource limit set for the container (e.g. `-Xmx4g`). In your calculation,
+    allow memory for other components running in the container.
+
+To enable liveness- and readiness probes, the healthcheck plugin will be installed
+by default. Note, that by configuring to use a packaged or downloaded version of
+the healthcheck plugin, the configured version will take precedence over the default
+version. The plugin is by default configured to disable the `querychanges` and
+`auth` healthchecks, since these would not work on a new and empty Gerrit server.
+The default configuration can be overwritten by adding the `healthcheck.config`
+file as a key-value pair to `gerrit.etc.config` as for every other configuration.
+
+SSH keys should be configured via the helm-chart using the `gerrit.etc.secret`
+map. Gerrit will create its own keys, if none are present in the site, but if
+multiple Gerrit pods are running, each Gerrit instance would have its own keys.
+Users accessing Gerrit via a load balancer would get issues due to changing
+host keys.
+
+### Installing Gerrit plugins
+
+There are several different ways to install plugins for Gerrit:
+
+- **RECOMMENDED: Package the plugins to install into the WAR-file containing Gerrit.**
+  This method provides the most stable way to install plugins, but requires to
+  use a custom built gerrit-war file and container images, if plugins are required
+  that are not part of the official `release.war`-file.
+
+- **Download and cache plugins.** The chart supports downloading the plugin files and
+  to cache them in a separate volume, that is shared between Gerrit-pods. SHA1-
+  sums are used to validate plugin-files and versions.
+
+- **Download plugins, but do not cache them.** This should only be used during
+  development to save resources (the shared volume). Each pod will download the
+  plugin-files on its own. Pods will fail to start up, if the download-URL is
+  not valid anymore at some point in time.
+
+## Upgrading the Chart
+
+To upgrade an existing installation of the gerrit chart, e.g. to install
+a newer chart version or to use an updated custom `values.yaml`-file, execute
+the following command:
+
+```sh
+cd $(git rev-parse --show-toplevel)/helm-charts
+helm upgrade \
+  <release-name> \
+  ./gerrit \ # path to chart
+  -f <path-to-custom-values>.yaml
+```
+
+## Uninstalling the Chart
+
+To delete the chart from the cluster, use:
+
+```sh
+helm delete <release-name>
+```
diff --git a/charts/k8s-gerrit/helm-charts/gerrit/docs/nfs-provisioner.md b/charts/k8s-gerrit/helm-charts/gerrit/docs/nfs-provisioner.md
new file mode 100644
index 0000000..9e83d47
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit/docs/nfs-provisioner.md
@@ -0,0 +1,64 @@
+# Installing a NFS-provisioner
+
+Gerrit requires access to a persistent volume capable of running in
+`Read Write Many (RWM)`-mode to store the git repositories, since the repositories
+have to be accessed by mutiple pods. One possibility to provide such volumes
+is to install a provisioner for NFS-volumes into the same Kubernetes-cluster.
+This document will guide through the process.
+
+The [Kubernetes external-storage project](https://github.com/kubernetes-incubator/external-storage)
+provides an out-of-tree dynamic [provisioner](https://github.com/kubernetes-incubator/external-storage/tree/master/nfs)
+for NFS volumes. A chart exists for easy deployment of the project onto a
+Kubernetes cluster. The chart's sources can be found [here](https://github.com/helm/charts/tree/master/stable/nfs-server-provisioner).
+
+## Prerequisites
+
+This guide will use Helm to install the NFS-provisioner. Thus, Helm has to be
+installed.
+
+## Installing the nfs-server-provisioner chart
+
+A custom `values.yaml`-file containing a configuration tested with the
+gerrit charts can be found in the `supplements/nfs`-directory in the
+gerrit chart's root directory. In addition a file stating the tested
+version of the nfs-server-provisioner chart is present in the same directory.
+
+If needed, adapt the `values.yaml`-file for the nfs-server-provisioner chart
+further and then run:
+
+```sh
+cd $(git rev-parse --show-toplevel)/helm-charts/gerrit/supplements/nfs
+helm install nfs \
+  stable/nfs-server-provisioner \
+  -f values.yaml \
+  --version $(cat VERSION)
+```
+
+For a description of the configuration options, refer to the
+[chart's documentation](https://github.com/helm/charts/blob/master/stable/nfs-server-provisioner/README.md).
+
+Here are some tips for configuring the nfs-server-provisioner chart to work with
+the gerrit chart:
+
+- Deploying more than 1 `replica` led to some reliability issues in tests and
+  should be further tested for now, if required.
+- The name of the StorageClass created for NFS-volumes has to be the same as the
+  one defined in the gerrit chart for `storageClasses.shared.name`
+- The StorageClas for NFS-volumes needs to have the parameter `mountOptions: vers=4.1`,
+  due to compatibility [issues](https://github.com/kubernetes-incubator/external-storage/issues/223)
+  with Ganesha.
+
+## Deleting the nfs-server-provisioner chart
+
+***note
+**Attention:** Never delete the nfs-server-provisioner chart, if there is still a
+PersistentVolumeClaim and Pods using a NFS-volume provisioned by the NFS server
+provisioner. This will lead to crashed pods, that will not be terminated correctly.
+***
+
+If no Pod or PVC is using a NFS-volume provisioned by the NFS server provisioner
+anymore, delete it like any other chart:
+
+```sh
+helm delete nfs
+```
diff --git a/charts/k8s-gerrit/helm-charts/gerrit/supplements/nfs/VERSION b/charts/k8s-gerrit/helm-charts/gerrit/supplements/nfs/VERSION
new file mode 100644
index 0000000..7dff5b8
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit/supplements/nfs/VERSION
@@ -0,0 +1 @@
+0.2.1
\ No newline at end of file
diff --git a/charts/k8s-gerrit/helm-charts/gerrit/supplements/nfs/values.yaml b/charts/k8s-gerrit/helm-charts/gerrit/supplements/nfs/values.yaml
new file mode 100644
index 0000000..a413d8a
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit/supplements/nfs/values.yaml
@@ -0,0 +1,42 @@
+# Deploying more than 1 `replica` led to some reliability issues in tests and
+# should be further tested for now, if required.
+replicaCount: 1
+
+image:
+  repository: quay.io/kubernetes_incubator/nfs-provisioner
+  tag: v1.0.9
+  pullPolicy: IfNotPresent
+
+service:
+  type: ClusterIP
+  nfsPort: 2049
+  mountdPort: 20048
+  rpcbindPort: 51413
+
+persistence:
+  enabled: true
+  storageClass: default
+  accessMode: ReadWriteOnce
+  size: 7.5Gi
+
+storageClass:
+  create: true
+  defaultClass: false
+  # The name of the StorageClass has to be the same as the one defined in the
+  # gerrit chart for `storageClasses.shared.name`
+  name: shared-storage
+  parameters:
+    # Required!
+    mountOptions: vers=4.1
+  reclaimPolicy: Delete
+
+rbac:
+  create: true
+
+resources:
+  requests:
+    cpu: 100m
+    memory: 256Mi
+  limits:
+    cpu: 100m
+    memory: 256Mi
diff --git a/charts/k8s-gerrit/helm-charts/gerrit/templates/NOTES.txt b/charts/k8s-gerrit/helm-charts/gerrit/templates/NOTES.txt
new file mode 100644
index 0000000..b71b3b0
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit/templates/NOTES.txt
@@ -0,0 +1,4 @@
+A primary Gerrit instance has been deployed.
+==================================
+
+Gerrit may be accessed under: {{ .Values.ingress.host }}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit/templates/_helpers.tpl b/charts/k8s-gerrit/helm-charts/gerrit/templates/_helpers.tpl
new file mode 100644
index 0000000..bace6fe
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit/templates/_helpers.tpl
@@ -0,0 +1,20 @@
+{{/*
+Create chart name and version as used by the chart label.
+*/}}
+{{- define "gerrit.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
+
+{{/*
+Create secret to access docker registry
+*/}}
+{{- define "imagePullSecret" }}
+{{- printf "{\"auths\": {\"%s\": {\"auth\": \"%s\"}}}" .Values.images.registry.name (printf "%s:%s" .Values.images.registry.ImagePullSecret.username .Values.images.registry.ImagePullSecret.password | b64enc) | b64enc }}
+{{- end }}
+
+{{/*
+Add '/' to registry if needed.
+*/}}
+{{- define "registry" -}}
+{{ if .Values.images.registry.name }}{{- printf "%s/" .Values.images.registry.name -}}{{end}}
+{{- end -}}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit/templates/gerrit.configmap.yaml b/charts/k8s-gerrit/helm-charts/gerrit/templates/gerrit.configmap.yaml
new file mode 100644
index 0000000..83c188c
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit/templates/gerrit.configmap.yaml
@@ -0,0 +1,78 @@
+{{- $root := . -}}
+
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: {{ .Release.Name }}-gerrit-configmap
+  labels:
+    app.kubernetes.io/component: gerrit
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+data:
+  {{- range $key, $value := .Values.gerrit.etc.config }}
+  {{ $key }}:
+{{ toYaml $value | indent 4 }}
+  {{- end }}
+  {{- if not (hasKey .Values.gerrit.etc.config "healthcheck.config") }}
+  healthcheck.config: |-
+    [healthcheck "auth"]
+      # On new instances there may be no users to use for healthchecks
+      enabled = false
+    [healthcheck "querychanges"]
+      # On new instances there won't be any changes to query
+      enabled = false
+  {{- end }}
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: {{ .Release.Name }}-gerrit-init-configmap
+  labels:
+    app.kubernetes.io/component: gerrit
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+data:
+  gerrit-init.yaml: |-
+    {{ if .Values.caCert -}}
+    caCertPath: /var/config/ca.crt
+    {{- end }}
+    pluginCacheEnabled: {{ .Values.gerrit.pluginManagement.cache.enabled }}
+    pluginCacheDir: /var/mnt/plugins
+    {{- if .Values.gerrit.pluginManagement.plugins }}
+    plugins:
+{{ toYaml .Values.gerrit.pluginManagement.plugins | indent 6}}
+    {{- end }}
+    {{- if .Values.gerrit.pluginManagement.libs }}
+    libs:
+{{ toYaml .Values.gerrit.pluginManagement.libs | indent 6}}
+    {{- end }}
+{{- range .Values.gerrit.additionalConfigMaps -}}
+{{- if .data }}
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name:  {{ $root.Release.Name }}-{{ .name }}
+  labels:
+    app.kubernetes.io/component: gerrit
+    app.kubernetes.io/instance: {{ $root.Release.Name }}
+    chart: {{ template "gerrit.chart" $root }}
+    heritage: {{ $root.Release.Service }}
+    release: {{ $root.Release.Name }}
+    {{- if $root.Values.additionalLabels }}
+{{ toYaml $root.Values.additionalLabels | indent 4 }}
+    {{- end }}
+data:
+{{ toYaml .data | indent 2 }}
+{{- end }}
+{{- end }}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit/templates/gerrit.secrets.yaml b/charts/k8s-gerrit/helm-charts/gerrit/templates/gerrit.secrets.yaml
new file mode 100644
index 0000000..72cfad3
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit/templates/gerrit.secrets.yaml
@@ -0,0 +1,21 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  name:  {{ .Release.Name }}-gerrit-secure-config
+  labels:
+    app.kubernetes.io/component: gerrit
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+data:
+  {{ if .Values.gerrit.keystore -}}
+  keystore: {{ .Values.gerrit.keystore }}
+  {{- end }}
+  {{- range $key, $value := .Values.gerrit.etc.secret }}
+  {{ $key }}: {{ $value | b64enc }}
+  {{- end }}
+type: Opaque
diff --git a/charts/k8s-gerrit/helm-charts/gerrit/templates/gerrit.service.yaml b/charts/k8s-gerrit/helm-charts/gerrit/templates/gerrit.service.yaml
new file mode 100644
index 0000000..fe16d45
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit/templates/gerrit.service.yaml
@@ -0,0 +1,41 @@
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ .Release.Name }}-gerrit-service
+  labels:
+    app.kubernetes.io/component: gerrit
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+  {{- if .Values.gerrit.service.additionalAnnotations }}
+  annotations:
+{{ toYaml .Values.gerrit.service.additionalAnnotations  | indent 4 }}
+  {{- end }}
+spec:
+  {{ with .Values.gerrit.service }}
+  {{- if .loadBalancerSourceRanges -}}
+  loadBalancerSourceRanges:
+{{- range .loadBalancerSourceRanges }}
+    - {{ . | quote }}
+{{- end }}
+  {{- end }}
+  ports:
+  - name: http
+    port: {{ .http.port }}
+    targetPort: 8080
+  {{- if .ssh.enabled }}
+  - name: ssh
+    port: {{ .ssh.port }}
+    targetPort: 29418
+  {{- end }}
+  type: {{ .type }}
+  externalTrafficPolicy: {{ .externalTrafficPolicy }}
+  {{- end }}
+  selector:
+    app.kubernetes.io/component: gerrit
+    app.kubernetes.io/instance: {{ .Release.Name }}
+
diff --git a/charts/k8s-gerrit/helm-charts/gerrit/templates/gerrit.stateful-set.yaml b/charts/k8s-gerrit/helm-charts/gerrit/templates/gerrit.stateful-set.yaml
new file mode 100644
index 0000000..a02b69e
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit/templates/gerrit.stateful-set.yaml
@@ -0,0 +1,290 @@
+{{- $root := . -}}
+
+apiVersion: apps/v1
+kind: StatefulSet
+metadata:
+  name: {{ .Release.Name }}-gerrit-stateful-set
+  labels:
+    app.kubernetes.io/component: gerrit
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+spec:
+  serviceName: {{ .Release.Name }}-gerrit-service
+  replicas: {{ .Values.gerrit.replicas }}
+  updateStrategy:
+    rollingUpdate:
+      partition: {{ .Values.gerrit.updatePartition }}
+  selector:
+    matchLabels:
+      app.kubernetes.io/component: gerrit
+      app.kubernetes.io/instance: {{ .Release.Name }}
+  template:
+    metadata:
+      labels:
+        app.kubernetes.io/component: gerrit
+        app.kubernetes.io/instance: {{ .Release.Name }}
+        chart: {{ template "gerrit.chart" . }}
+        heritage: {{ .Release.Service }}
+        release: {{ .Release.Name }}
+        {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 8 }}
+        {{- end }}
+        {{- if .Values.gerrit.additionalPodLabels }}
+{{ toYaml .Values.gerrit.additionalPodLabels  | indent 8 }}
+        {{- end }}
+      annotations:
+        chartRevision: "{{ .Release.Revision }}"
+        {{- if .Values.gerrit.additionalAnnotations }}
+{{ toYaml .Values.gerrit.additionalAnnotations  | indent 8 }}
+        {{- end }}
+    spec:
+      {{- with .Values.gerrit.tolerations }}
+      tolerations:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      {{- with .Values.gerrit.topologySpreadConstraints }}
+      topologySpreadConstraints:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      {{- with .Values.gerrit.nodeSelector }}
+      nodeSelector:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      {{- with .Values.gerrit.affinity }}
+      affinity:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      {{- with .Values.gerrit.priorityClassName }}
+      priorityClassName: {{ . }}
+      {{- end }}
+      terminationGracePeriodSeconds: {{ .Values.gerrit.gracefulStopTimeout }}
+      securityContext:
+        fsGroup: 100
+      {{ if .Values.images.registry.ImagePullSecret.name -}}
+      imagePullSecrets:
+      - name: {{ .Values.images.registry.ImagePullSecret.name }}
+      {{- range .Values.images.additionalImagePullSecrets }}
+      - name: {{ . }}
+      {{- end }}
+      {{- end }}
+      initContainers:
+      {{- if and .Values.nfsWorkaround.enabled .Values.nfsWorkaround.chownOnStartup }}
+      - name: nfs-init
+        image: {{ .Values.images.busybox.registry -}}/busybox:{{- .Values.images.busybox.tag }}
+        command:
+        - sh
+        - -c
+        args:
+        - |
+          chown 1000:100 /var/mnt/logs
+          chown 1000:100 /var/mnt/git
+        env:
+        - name: POD_NAME
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.name
+        volumeMounts:
+        - name: logs
+          subPathExpr: "gerrit-replica/$(POD_NAME)"
+          mountPath: "/var/mnt/logs"
+        - name: git-repositories
+          mountPath: "/var/mnt/git"
+        {{- if .Values.nfsWorkaround.idDomain }}
+        - name: nfs-config
+          mountPath: "/etc/idmapd.conf"
+          subPath: idmapd.conf
+        {{- end }}
+      {{- end }}
+      - name: gerrit-init
+        image: {{ template "registry" . }}{{ .Values.gerrit.images.gerritInit }}:{{ .Values.images.version }}
+        imagePullPolicy: {{ .Values.images.imagePullPolicy }}
+        env:
+        - name: POD_NAME
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.name
+        volumeMounts:
+        - name: gerrit-site
+          mountPath: "/var/gerrit"
+        - name: git-repositories
+          mountPath: "/var/mnt/git"
+        - name: logs
+          subPathExpr: "gerrit/$(POD_NAME)"
+          mountPath: "/var/mnt/logs"
+        - name: gerrit-init-config
+          mountPath: "/var/config/gerrit-init.yaml"
+          subPath: gerrit-init.yaml
+        {{- if and .Values.nfsWorkaround.enabled .Values.nfsWorkaround.idDomain }}
+        - name: nfs-config
+          mountPath: "/etc/idmapd.conf"
+          subPath: idmapd.conf
+        {{- end }}
+        {{- if and .Values.gerrit.pluginManagement.cache.enabled }}
+        - name: gerrit-plugin-cache
+          mountPath: "/var/mnt/plugins"
+        {{- end }}
+        {{ if eq .Values.gerrit.index.type "elasticsearch" -}}
+        - name: gerrit-index-config
+          mountPath: "/var/mnt/index"
+        {{- end }}
+        - name: gerrit-config
+          mountPath: "/var/mnt/etc/config"
+        - name: gerrit-secure-config
+          mountPath: "/var/mnt/etc/secret"
+        {{ if .Values.caCert -}}
+        - name: tls-ca
+          subPath: ca.crt
+          mountPath: "/var/config/ca.crt"
+        {{- end }}
+        {{- range .Values.gerrit.additionalConfigMaps }}
+        - name: {{ .name }}
+          mountPath: "/var/mnt/data/{{ .subDir }}"
+        {{- end }}
+      containers:
+      - name: gerrit
+        image: {{ template "registry" . }}{{ .Values.gerrit.images.gerrit }}:{{ .Values.images.version }}
+        imagePullPolicy: {{ .Values.images.imagePullPolicy }}
+        env:
+        - name: POD_NAME
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.name
+        lifecycle:
+          preStop:
+            exec:
+              command:
+                - "/bin/ash"
+                - "-c"
+                - "kill -2 $(pidof java) && tail --pid=$(pidof java) -f /dev/null"
+        ports:
+        - name: gerrit-port
+          containerPort: 8080
+        {{- if .Values.gerrit.service.ssh.enabled }}
+        - name: gerrit-ssh
+          containerPort: 29418
+        {{- end }}
+        volumeMounts:
+        - name: gerrit-site
+          mountPath: "/var/gerrit"
+        - name: git-repositories
+          mountPath: "/var/mnt/git"
+        - name: logs
+          subPathExpr: "gerrit/$(POD_NAME)"
+          mountPath: "/var/mnt/logs"
+        {{- if and .Values.nfsWorkaround.enabled .Values.nfsWorkaround.idDomain }}
+        - name: nfs-config
+          mountPath: "/etc/idmapd.conf"
+          subPath: idmapd.conf
+        {{- end }}
+        {{ if eq .Values.gerrit.index.type "elasticsearch" -}}
+        - name: gerrit-index-config
+          mountPath: "/var/mnt/index"
+        {{- end }}
+        - name: gerrit-config
+          mountPath: "/var/mnt/etc/config"
+        - name: gerrit-secure-config
+          mountPath: "/var/mnt/etc/secret"
+        {{- range .Values.gerrit.additionalConfigMaps }}
+        - name: {{ .name }}
+          mountPath: "/var/mnt/data/{{ .subDir }}"
+        {{- end }}
+        resources:
+{{ toYaml .Values.gerrit.resources | indent 10 }}
+        livenessProbe:
+          httpGet:
+            path: /config/server/healthcheck~status
+            port: gerrit-port
+{{ toYaml .Values.gerrit.livenessProbe | indent 10 }}
+        readinessProbe:
+          httpGet:
+            path: /config/server/healthcheck~status
+            port: gerrit-port
+{{ toYaml .Values.gerrit.readinessProbe | indent 10 }}
+        startupProbe:
+          httpGet:
+            path: /config/server/healthcheck~status
+            port: gerrit-port
+{{ toYaml .Values.gerrit.startupProbe | indent 10 }}
+      volumes:
+      {{ if not .Values.gerrit.persistence.enabled -}}
+      - name: gerrit-site
+        emptyDir: {}
+      {{- end }}
+      {{- if and .Values.gerrit.pluginManagement.cache.enabled }}
+      - name: gerrit-plugin-cache
+        persistentVolumeClaim:
+          claimName: {{ .Release.Name }}-plugin-cache-pvc
+      {{- end }}
+      - name: git-repositories
+        persistentVolumeClaim:
+          {{- if .Values.gitRepositoryStorage.externalPVC.use }}
+          claimName: {{ .Values.gitRepositoryStorage.externalPVC.name }}
+          {{- else }}
+          claimName: {{ .Release.Name }}-git-repositories-pvc
+          {{- end }}
+      - name: logs
+        {{ if .Values.logStorage.enabled -}}
+        persistentVolumeClaim:
+          {{- if .Values.logStorage.externalPVC.use }}
+          claimName: {{ .Values.logStorage.externalPVC.name }}
+          {{- else }}
+          claimName: {{ .Release.Name }}-log-pvc
+          {{- end }}
+        {{ else -}}
+        emptyDir: {}
+        {{- end }}
+      - name: gerrit-init-config
+        configMap:
+          name: {{ .Release.Name }}-gerrit-init-configmap
+      {{ if eq .Values.gerrit.index.type "elasticsearch" -}}
+      - name: gerrit-index-config
+        persistentVolumeClaim:
+          claimName: {{ .Release.Name }}-gerrit-index-config-pvc
+      {{- end }}
+      - name: gerrit-config
+        configMap:
+          name: {{ .Release.Name }}-gerrit-configmap
+      - name: gerrit-secure-config
+        secret:
+          secretName: {{ .Release.Name }}-gerrit-secure-config
+      {{ if .Values.caCert -}}
+      - name: tls-ca
+        secret:
+          secretName: {{ .Release.Name }}-tls-ca
+      {{- end }}
+      {{- range .Values.gerrit.additionalConfigMaps }}
+      - name: {{ .name }}
+        configMap:
+          name: {{ if .data }}{{ $root.Release.Name }}-{{ .name }}{{ else }}{{ .name }}{{ end }}
+      {{- end }}
+      {{- if and .Values.nfsWorkaround.enabled .Values.nfsWorkaround.idDomain }}
+      - name: nfs-config
+        configMap:
+          name: {{ .Release.Name }}-nfs-configmap
+      {{- end }}
+  {{ if .Values.gerrit.persistence.enabled -}}
+  volumeClaimTemplates:
+  - metadata:
+      name: gerrit-site
+      labels:
+        app.kubernetes.io/component: gerrit
+        app.kubernetes.io/instance: {{ .Release.Name }}
+        chart: {{ template "gerrit.chart" . }}
+        heritage: {{ .Release.Service }}
+        release: {{ .Release.Name }}
+        {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 8 }}
+        {{- end }}
+    spec:
+      accessModes:
+      - ReadWriteOnce
+      resources:
+        requests:
+          storage: {{ .Values.gerrit.persistence.size }}
+      storageClassName: {{ .Values.storageClasses.default.name }}
+  {{- end }}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit/templates/gerrit.storage.yaml b/charts/k8s-gerrit/helm-charts/gerrit/templates/gerrit.storage.yaml
new file mode 100644
index 0000000..1d85fc6
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit/templates/gerrit.storage.yaml
@@ -0,0 +1,45 @@
+{{- if and .Values.gerrit.pluginManagement.cache.enabled }}
+kind: PersistentVolumeClaim
+apiVersion: v1
+metadata:
+  name: {{ .Release.Name }}-plugin-cache-pvc
+  labels:
+    app.kubernetes.io/component: gerrit
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+spec:
+  accessModes:
+  - ReadWriteMany
+  resources:
+    requests:
+      storage: {{ .Values.gerrit.pluginManagement.cache.size }}
+  storageClassName: {{ .Values.storageClasses.shared.name }}
+{{- end }}
+{{ if eq .Values.gerrit.index.type "elasticsearch" -}}
+---
+kind: PersistentVolumeClaim
+apiVersion: v1
+metadata:
+  name: {{ .Release.Name }}-gerrit-index-config-pvc
+  labels:
+    app.kubernetes.io/component: gerrit
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+spec:
+  accessModes:
+  - ReadWriteMany
+  resources:
+    requests:
+      storage: 10Mi
+  storageClassName: {{ .Values.storageClasses.shared.name }}
+{{- end }}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit/templates/git-gc.cronjob.yaml b/charts/k8s-gerrit/helm-charts/gerrit/templates/git-gc.cronjob.yaml
new file mode 100644
index 0000000..8230e5d
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit/templates/git-gc.cronjob.yaml
@@ -0,0 +1,132 @@
+apiVersion: batch/v1
+kind: CronJob
+metadata:
+  name: {{ .Release.Name }}-git-gc
+  labels:
+    app.kubernetes.io/component: git-gc
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+spec:
+  schedule: {{ .Values.gitGC.schedule | quote }}
+  concurrencyPolicy: "Forbid"
+  jobTemplate:
+    spec:
+      template:
+        metadata:
+          labels:
+            app.kubernetes.io/component: git-gc
+            app.kubernetes.io/instance: {{ .Release.Name }}
+            chart: {{ template "gerrit.chart" . }}
+            heritage: {{ .Release.Service }}
+            release: {{ .Release.Name }}
+            {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 12 }}
+            {{- end }}
+            {{- if .Values.gitGC.additionalPodLabels }}
+{{ toYaml .Values.gitGC.additionalPodLabels  | indent 12 }}
+            {{- end }}
+          annotations:
+            cluster-autoscaler.kubernetes.io/safe-to-evict: "false"
+        spec:
+          {{- with .Values.gitGC.tolerations }}
+          tolerations:
+            {{- toYaml . | nindent 10 }}
+          {{- end }}
+          {{- with .Values.gitGC.nodeSelector }}
+          nodeSelector:
+            {{- toYaml . | nindent 12 }}
+          {{- end }}
+          {{- with .Values.gitGC.affinity }}
+          affinity:
+            {{- toYaml . | nindent 12 }}
+          {{- end }}
+          restartPolicy: OnFailure
+          securityContext:
+            runAsUser: 1000
+            fsGroup: 100
+          {{ if .Values.images.registry.ImagePullSecret.name -}}
+          imagePullSecrets:
+          - name: {{ .Values.images.registry.ImagePullSecret.name }}
+          {{- range .Values.images.additionalImagePullSecrets }}
+          - name: {{ . }}
+          {{- end }}
+          {{- end }}
+          initContainers:
+          {{- if and .Values.nfsWorkaround.enabled .Values.nfsWorkaround.chownOnStartup }}
+          - name: nfs-init
+            image: {{ .Values.images.busybox.registry -}}/busybox:{{- .Values.images.busybox.tag }}
+            command:
+            - sh
+            - -c
+            args:
+            - |
+              chown 1000:100 /var/mnt/logs
+              chown 1000:100 /var/mnt/git
+            env:
+            - name: POD_NAME
+              valueFrom:
+                fieldRef:
+                  fieldPath: metadata.name
+            volumeMounts:
+            - name: logs
+              subPathExpr: "git-gc/$(POD_NAME)"
+              mountPath: "/var/mnt/logs"
+            - name: git-repositories
+              mountPath: "/var/mnt/git"
+            {{- if .Values.nfsWorkaround.idDomain }}
+            - name: nfs-config
+              mountPath: "/etc/idmapd.conf"
+              subPath: idmapd.conf
+            {{- end }}
+          {{- end }}
+          containers:
+          - name: git-gc
+            imagePullPolicy: {{ .Values.images.imagePullPolicy }}
+            image: {{ template "registry" . }}{{ .Values.gitGC.image }}:{{ .Values.images.version }}
+            env:
+            - name: POD_NAME
+              valueFrom:
+                fieldRef:
+                  fieldPath: metadata.name
+            resources:
+{{ toYaml .Values.gitGC.resources | indent 14 }}
+            volumeMounts:
+            - name: git-repositories
+              mountPath: "/var/gerrit/git"
+            - name: logs
+              subPathExpr: "git-gc/$(POD_NAME)"
+              mountPath: "/var/log/git"
+            {{- if and .Values.nfsWorkaround.enabled .Values.nfsWorkaround.idDomain }}
+            - name: nfs-config
+              mountPath: "/etc/idmapd.conf"
+              subPath: idmapd.conf
+            {{- end }}
+          volumes:
+          - name: git-repositories
+            persistentVolumeClaim:
+              {{- if .Values.gitRepositoryStorage.externalPVC.use }}
+              claimName: {{ .Values.gitRepositoryStorage.externalPVC.name }}
+              {{- else }}
+              claimName: {{ .Release.Name }}-git-repositories-pvc
+              {{- end }}
+          - name: logs
+            {{ if .Values.logStorage.enabled -}}
+            persistentVolumeClaim:
+              {{- if .Values.logStorage.externalPVC.use }}
+              claimName: {{ .Values.logStorage.externalPVC.name }}
+              {{- else }}
+              claimName: {{ .Release.Name }}-log-pvc
+              {{- end }}
+            {{ else -}}
+            emptyDir: {}
+            {{- end }}
+          {{- if and .Values.nfsWorkaround.enabled .Values.nfsWorkaround.idDomain }}
+          - name: nfs-config
+            configMap:
+              name: {{ .Release.Name }}-nfs-configmap
+          {{- end }}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit/templates/git-gc.storage.yaml b/charts/k8s-gerrit/helm-charts/gerrit/templates/git-gc.storage.yaml
new file mode 100644
index 0000000..c69a647
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit/templates/git-gc.storage.yaml
@@ -0,0 +1,22 @@
+{{ if .Values.gitGC.logging.persistence.enabled -}}
+kind: PersistentVolumeClaim
+apiVersion: v1
+metadata:
+  name: {{ .Release.Name }}-git-gc-logs-pvc
+  labels:
+    app.kubernetes.io/component: git-gc
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+spec:
+  accessModes:
+  - ReadWriteOnce
+  resources:
+    requests:
+      storage: {{ .Values.gitGC.logging.persistence.size }}
+  storageClassName: {{ .Values.storageClasses.default.name }}
+{{- end }}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit/templates/global.secrets.yaml b/charts/k8s-gerrit/helm-charts/gerrit/templates/global.secrets.yaml
new file mode 100644
index 0000000..b2c3d5d
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit/templates/global.secrets.yaml
@@ -0,0 +1,18 @@
+{{ if .Values.caCert -}}
+apiVersion: v1
+kind: Secret
+metadata:
+  name:  {{ .Release.Name }}-tls-ca
+  labels:
+    app.kubernetes.io/component: gerrit
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+data:
+  ca.crt: {{ .Values.caCert | b64enc }}
+type: Opaque
+{{- end }}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit/templates/image-pull.secret.yaml b/charts/k8s-gerrit/helm-charts/gerrit/templates/image-pull.secret.yaml
new file mode 100644
index 0000000..d107472
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit/templates/image-pull.secret.yaml
@@ -0,0 +1,9 @@
+{{ if and .Values.images.registry.ImagePullSecret.name .Values.images.registry.ImagePullSecret.create -}}
+apiVersion: v1
+kind: Secret
+metadata:
+  name: {{ .Values.images.registry.ImagePullSecret.name }}
+type: kubernetes.io/dockerconfigjson
+data:
+  .dockerconfigjson: {{ template "imagePullSecret" . }}
+{{- end }}
\ No newline at end of file
diff --git a/charts/k8s-gerrit/helm-charts/gerrit/templates/ingress.yaml b/charts/k8s-gerrit/helm-charts/gerrit/templates/ingress.yaml
new file mode 100644
index 0000000..eb19655
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit/templates/ingress.yaml
@@ -0,0 +1,64 @@
+{{- if .Values.ingress.enabled }}
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: {{ .Release.Name }}-gerrit-ingress
+  labels:
+    app.kubernetes.io/component: gerrit
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.ingress.additionalLabels }}
+{{ toYaml .Values.ingress.additionalLabels  | indent 4 }}
+    {{- end }}
+  annotations:
+    nginx.ingress.kubernetes.io/proxy-body-size: {{ .Values.ingress.maxBodySize | default "50m" }}
+    {{- if .Values.ingress.additionalAnnotations }}
+{{ toYaml .Values.ingress.additionalAnnotations  | indent 4 }}
+    {{- end }}
+spec:
+  {{ if .Values.ingress.tls.enabled -}}
+  tls:
+  - hosts:
+    - {{ .Values.ingress.host }}
+    {{ if .Values.ingress.tls.secret.create -}}
+    secretName: {{ .Release.Name }}-gerrit-tls-secret
+    {{- else }}
+    secretName: {{ .Values.ingress.tls.secret.name }}
+    {{- end }}
+  {{- end }}
+  rules:
+  - host: {{required "A host URL is required for the Gerrit Ingress. Please set 'ingress.host'" .Values.ingress.host }}
+    http:
+      paths:
+      - pathType: Prefix
+        path: /
+        backend:
+          service:
+            name: {{ .Release.Name }}-gerrit-service
+            port:
+              number: {{ .Values.gerrit.service.http.port }}
+{{- end }}
+---
+{{ if and .Values.ingress.tls.enabled .Values.ingress.tls.secret.create -}}
+apiVersion: v1
+kind: Secret
+metadata:
+  name:  {{ .Release.Name }}-gerrit-tls-secret
+  labels:
+    app.kubernetes.io/component: gerrit
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.ingress.additionalLabels }}
+{{ toYaml .Values.ingress.additionalLabels  | indent 4 }}
+    {{- end }}
+type: kubernetes.io/tls
+data:
+  {{ with .Values.ingress.tls -}}
+  tls.crt: {{ .cert | b64enc }}
+  tls.key: {{ .key | b64enc }}
+  {{- end }}
+{{- end }}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit/templates/log-cleaner.cronjob.yaml b/charts/k8s-gerrit/helm-charts/gerrit/templates/log-cleaner.cronjob.yaml
new file mode 100644
index 0000000..c1314f1
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit/templates/log-cleaner.cronjob.yaml
@@ -0,0 +1,65 @@
+{{- if and .Values.logStorage.enabled .Values.logStorage.cleanup.enabled }}
+apiVersion: batch/v1beta1
+kind: CronJob
+metadata:
+  name: {{ .Release.Name }}-log-cleaner
+  labels:
+    app.kubernetes.io/component: log-cleaner
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+spec:
+  schedule: {{ .Values.logStorage.cleanup.schedule | quote }}
+  concurrencyPolicy: "Forbid"
+  jobTemplate:
+    spec:
+      template:
+        metadata:
+          labels:
+            app.kubernetes.io/component: log-cleaner
+            app.kubernetes.io/instance: {{ .Release.Name }}
+            chart: {{ template "gerrit.chart" . }}
+            heritage: {{ .Release.Service }}
+            release: {{ .Release.Name }}
+            {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 12 }}
+            {{- end }}
+            {{- if .Values.logStorage.cleanup.additionalPodLabels }}
+{{ toYaml .Values.logStorage.cleanup.additionalPodLabels  | indent 12 }}
+            {{- end }}
+        spec:
+          restartPolicy: OnFailure
+          containers:
+          - name: log-cleaner
+            imagePullPolicy: {{ .Values.images.imagePullPolicy }}
+            image: {{ .Values.images.busybox.registry -}}/busybox:{{- .Values.images.busybox.tag }}
+            command:
+            - sh
+            - -c
+            args:
+            - |
+              find /var/logs/ \
+                -mindepth 1 \
+                -type f \
+                -mtime +{{ .Values.logStorage.cleanup.retentionDays }} \
+                -print \
+                -delete
+              find /var/logs/ -type d -empty -delete
+            resources:
+{{ toYaml .Values.logStorage.cleanup.resources | indent 14 }}
+            volumeMounts:
+            - name: logs
+              mountPath: "/var/logs"
+          volumes:
+          - name: logs
+            persistentVolumeClaim:
+              {{- if .Values.logStorage.externalPVC.use }}
+              claimName: {{ .Values.logStorage.externalPVC.name }}
+              {{- else }}
+              claimName: {{ .Release.Name }}-log-pvc
+              {{- end }}
+{{- end }}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit/templates/netpol.yaml b/charts/k8s-gerrit/helm-charts/gerrit/templates/netpol.yaml
new file mode 100644
index 0000000..c0cbc4d
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit/templates/netpol.yaml
@@ -0,0 +1,122 @@
+{{ if .Values.networkPolicies.enabled -}}
+kind: NetworkPolicy
+apiVersion: networking.k8s.io/v1
+metadata:
+  name: {{ .Release.Name }}-default-deny-all
+  labels:
+    chart: {{ template "gerrit.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.networkPolicies.additionalLabels }}
+{{ toYaml .Values.networkPolicies.additionalLabels  | indent 4 }}
+    {{- end }}
+spec:
+  podSelector:
+    matchLabels:
+      chart: {{ template "gerrit.chart" . }}
+      release: {{ .Release.Name }}
+  policyTypes:
+  - Ingress
+  - Egress
+  ingress: []
+  egress: []
+---
+{{ if .Values.networkPolicies.dnsPorts -}}
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: {{ .Release.Name }}-allow-dns-access
+  labels:
+    chart: {{ template "gerrit.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.networkPolicies.additionalLabels }}
+{{ toYaml .Values.networkPolicies.additionalLabels  | indent 4 }}
+    {{- end }}
+spec:
+  podSelector:
+    matchLabels:
+      chart: {{ template "gerrit.chart" . }}
+      release: {{ .Release.Name }}
+  policyTypes:
+  - Egress
+  egress:
+  - ports:
+    {{ range .Values.networkPolicies.dnsPorts -}}
+    - port: {{ . }}
+      protocol: UDP
+    - port: {{ . }}
+      protocol: TCP
+    {{ end }}
+{{- end }}
+---
+kind: NetworkPolicy
+apiVersion: networking.k8s.io/v1
+metadata:
+  name: gerrit-allow-external
+  labels:
+    app.kubernetes.io/component: gerrit
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+spec:
+  podSelector:
+    matchLabels:
+      chart: {{ template "gerrit.chart" . }}
+      release: {{ .Release.Name }}
+      app.kubernetes.io/component: gerrit
+      app.kubernetes.io/instance: {{ .Release.Name }}
+  ingress:
+  - ports:
+    - port: 8080
+    from: []
+---
+{{ if or .Values.gerrit.networkPolicy.ingress -}}
+kind: NetworkPolicy
+apiVersion: networking.k8s.io/v1
+metadata:
+  name: gerrit-custom-ingress-policies
+  labels:
+    app.kubernetes.io/component: gerrit
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+spec:
+  policyTypes:
+  - Ingress
+  podSelector:
+    matchLabels:
+      chart: {{ template "gerrit.chart" . }}
+      release: {{ .Release.Name }}
+      app.kubernetes.io/component: gerrit
+      app.kubernetes.io/instance: {{ .Release.Name }}
+  ingress:
+{{ toYaml .Values.gerrit.networkPolicy.ingress | indent 2 }}
+{{- end }}
+---
+{{ if or .Values.gerrit.networkPolicy.egress -}}
+kind: NetworkPolicy
+apiVersion: networking.k8s.io/v1
+metadata:
+  name: gerrit-custom-egress-policies
+  labels:
+    app.kubernetes.io/component: gerrit
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+spec:
+  policyTypes:
+  - Egress
+  podSelector:
+    matchLabels:
+      chart: {{ template "gerrit.chart" . }}
+      release: {{ .Release.Name }}
+      app.kubernetes.io/component: gerrit
+      app.kubernetes.io/instance: {{ .Release.Name }}
+  egress:
+{{ toYaml .Values.gerrit.networkPolicy.egress | indent 2 }}
+{{- end }}
+{{- end }}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit/templates/nfs.configmap.yaml b/charts/k8s-gerrit/helm-charts/gerrit/templates/nfs.configmap.yaml
new file mode 100644
index 0000000..dd2c3dd
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit/templates/nfs.configmap.yaml
@@ -0,0 +1,28 @@
+{{- if and .Values.nfsWorkaround.enabled .Values.nfsWorkaround.idDomain -}}
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: {{ .Release.Name }}-nfs-configmap
+  labels:
+    app.kubernetes.io/component: gerrit
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+data:
+  idmapd.conf: |-
+    [General]
+
+    Verbosity = 0
+    Pipefs-Directory = /run/rpc_pipefs
+    # set your own domain here, if it differs from FQDN minus hostname
+    Domain = {{ .Values.nfsWorkaround.idDomain }}
+
+    [Mapping]
+
+    Nobody-User = nobody
+    Nobody-Group = nogroup
+{{- end }}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit/templates/storage.pvc.yaml b/charts/k8s-gerrit/helm-charts/gerrit/templates/storage.pvc.yaml
new file mode 100644
index 0000000..b262402
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit/templates/storage.pvc.yaml
@@ -0,0 +1,45 @@
+{{- if not .Values.gitRepositoryStorage.externalPVC.use }}
+kind: PersistentVolumeClaim
+apiVersion: v1
+metadata:
+  name: {{ .Release.Name }}-git-repositories-pvc
+  labels:
+    app.kubernetes.io/component: gerrit
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+spec:
+  accessModes:
+  - ReadWriteMany
+  resources:
+    requests:
+      storage: {{ .Values.gitRepositoryStorage.size }}
+  storageClassName: {{ .Values.storageClasses.shared.name }}
+{{- end }}
+{{- if and .Values.logStorage.enabled (not .Values.logStorage.externalPVC.use) }}
+---
+kind: PersistentVolumeClaim
+apiVersion: v1
+metadata:
+  name: {{ .Release.Name }}-log-pvc
+  labels:
+    app.kubernetes.io/component: gerrit
+    app.kubernetes.io/instance: {{ .Release.Name }}
+    chart: {{ template "gerrit.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+spec:
+  accessModes:
+  - ReadWriteMany
+  resources:
+    requests:
+      storage: {{ .Values.logStorage.size }}
+  storageClassName: {{ .Values.storageClasses.shared.name }}
+{{- end }}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit/templates/storageclasses.yaml b/charts/k8s-gerrit/helm-charts/gerrit/templates/storageclasses.yaml
new file mode 100644
index 0000000..552cd6a
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit/templates/storageclasses.yaml
@@ -0,0 +1,53 @@
+{{ if .Values.storageClasses.default.create -}}
+kind: StorageClass
+apiVersion: storage.k8s.io/v1
+metadata:
+  name: {{ .Values.storageClasses.default.name }}
+  labels:
+    chart: {{ template "gerrit.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+provisioner: {{ .Values.storageClasses.default.provisioner }}
+reclaimPolicy: {{ .Values.storageClasses.default.reclaimPolicy }}
+{{ if .Values.storageClasses.default.parameters -}}
+parameters:
+{{- range $key, $value := .Values.storageClasses.default.parameters }}
+  {{ $key }}: {{ $value }}
+{{- end }}
+mountOptions:
+{{- range $value := .Values.storageClasses.default.mountOptions }}
+  - {{ $value }}
+{{- end }}
+allowVolumeExpansion: {{ .Values.storageClasses.default.allowVolumeExpansion }}
+{{- end }}
+{{- end }}
+---
+{{ if .Values.storageClasses.shared.create -}}
+kind: StorageClass
+apiVersion: storage.k8s.io/v1
+metadata:
+  name: {{ .Values.storageClasses.shared.name }}
+  labels:
+    chart: {{ template "gerrit.chart" . }}
+    heritage: {{ .Release.Service }}
+    release: {{ .Release.Name }}
+    {{- if .Values.additionalLabels }}
+{{ toYaml .Values.additionalLabels  | indent 4 }}
+    {{- end }}
+provisioner: {{ .Values.storageClasses.shared.provisioner }}
+reclaimPolicy: {{ .Values.storageClasses.shared.reclaimPolicy }}
+{{ if .Values.storageClasses.shared.parameters -}}
+parameters:
+{{- range $key, $value := .Values.storageClasses.shared.parameters }}
+  {{ $key }}: {{ $value }}
+{{- end }}
+mountOptions:
+{{- range $value := .Values.storageClasses.shared.mountOptions }}
+  - {{ $value }}
+{{- end }}
+allowVolumeExpansion: {{ .Values.storageClasses.shared.allowVolumeExpansion }}
+{{- end }}
+{{- end }}
diff --git a/charts/k8s-gerrit/helm-charts/gerrit/values.yaml b/charts/k8s-gerrit/helm-charts/gerrit/values.yaml
new file mode 100644
index 0000000..1135aa9
--- /dev/null
+++ b/charts/k8s-gerrit/helm-charts/gerrit/values.yaml
@@ -0,0 +1,333 @@
+images:
+  busybox:
+    registry: docker.io
+    tag: latest
+  # Registry used for container images created by this project
+  registry:
+    # The registry name must NOT contain a trailing slash
+    name:
+    ImagePullSecret:
+      # Leave blank, if no ImagePullSecret is needed.
+      name: image-pull-secret
+      # If set to false, the gerrit chart expects either a ImagePullSecret
+      # with the name configured above to be present on the cluster or that no
+      # credentials are needed.
+      create: false
+      username:
+      password:
+  version: latest
+  imagePullPolicy: Always
+  # Additional ImagePullSecrets that already exist and should be used by the
+  # pods of this chart. E.g. to pull busybox from dockerhub.
+  additionalImagePullSecrets: []
+
+# Additional labels that should be applied to all resources
+additionalLabels: {}
+
+storageClasses:
+  # Storage class used for storing logs and other pod-specific persisted data
+  default:
+    # If create is set to false, an existing StorageClass with the given
+    # name is expected to exist in the cluster. Setting create to true will
+    # create a storage class with the parameters given below.
+    name: default
+    create: false
+    provisioner: kubernetes.io/aws-ebs
+    reclaimPolicy: Delete
+    # Use the parameters key to set all parameters needed for the provisioner
+    parameters:
+      type: gp2
+      fsType: ext4
+    mountOptions: []
+    allowVolumeExpansion: false
+  # Storage class used for storing git repositories. Has to provide RWM access.
+  shared:
+    # If create is set to false, an existing StorageClass with RWM access
+    # mode and the given name has to be provided.
+    name: shared-storage
+    create: false
+    provisioner: nfs
+    reclaimPolicy: Delete
+    # Use the parameters key to set all parameters needed for the provisioner
+    parameters:
+      mountOptions: vers=4.1
+    mountOptions: []
+    allowVolumeExpansion: false
+
+
+nfsWorkaround:
+  enabled: false
+  chownOnStartup: false
+  idDomain: localdomain.com
+
+
+networkPolicies:
+  enabled: false
+  dnsPorts:
+  - 53
+  - 8053
+
+
+gitRepositoryStorage:
+  externalPVC:
+    use: false
+    name: git-repositories-pvc
+  size: 5Gi
+
+logStorage:
+  enabled: false
+  externalPVC:
+    use: false
+    name: gerrit-logs-pvc
+  size: 5Gi
+  cleanup:
+    enabled: false
+    additionalPodLabels: {}
+    schedule: "0 0 * * *"
+    retentionDays: 14
+    resources:
+      requests:
+        cpu: 100m
+        memory: 256Mi
+      limits:
+        cpu: 100m
+        memory: 256Mi
+
+caCert:
+
+ingress:
+  enabled: false
+  host:
+  # The maximum body size to allow for requests. Use "0" to allow unlimited
+  # reuqest body sizes.
+  maxBodySize: 50m
+  additionalAnnotations:
+    kubernetes.io/ingress.class: nginx
+  #  nginx.ingress.kubernetes.io/server-alias: example.com
+  #  nginx.ingress.kubernetes.io/whitelist-source-range: xxx.xxx.xxx.xxx
+  tls:
+    enabled: false
+    secret:
+      create: true
+      # `name` will only be used, if `create` is set to false to bind an
+      # existing secret. Otherwise the name will be automatically generated to
+      # avoid conflicts between multiple chart installations.
+      name:
+    # `cert`and `key` will only be used, if the secret will be created by
+    # this chart.
+    cert: |-
+      -----BEGIN CERTIFICATE-----
+
+      -----END CERTIFICATE-----
+    key: |-
+      -----BEGIN RSA PRIVATE KEY-----
+
+      -----END RSA PRIVATE KEY-----
+
+
+gitGC:
+  image: k8sgerrit/git-gc
+
+  tolerations: []
+  nodeSelector: {}
+  affinity: {}
+  additionalPodLabels: {}
+
+  schedule: 0 6,18 * * *
+
+  resources:
+    requests:
+      cpu: 100m
+      memory: 256Mi
+    limits:
+      cpu: 100m
+      memory: 256Mi
+
+  logging:
+    persistence:
+      enabled: true
+      size: 1Gi
+
+
+gerrit:
+  images:
+    gerritInit: k8sgerrit/gerrit-init
+    gerrit: k8sgerrit/gerrit
+
+  tolerations: []
+  topologySpreadConstraints: {}
+  nodeSelector: {}
+  affinity: {}
+  additionalAnnotations: {}
+  additionalPodLabels: {}
+
+  replicas: 1
+  updatePartition: 0
+
+  # The memory limit has to be higher than the configures heap-size for Java!
+  resources:
+    requests:
+      cpu: 1
+      memory: 5Gi
+    limits:
+      cpu: 1
+      memory: 6Gi
+
+  persistence:
+    enabled: true
+    size: 10Gi
+
+  livenessProbe:
+    initialDelaySeconds: 30
+    periodSeconds: 5
+
+  readinessProbe:
+    initialDelaySeconds: 5
+    periodSeconds: 1
+
+  startupProbe:
+    initialDelaySeconds: 10
+    periodSeconds: 30
+
+  gracefulStopTimeout: 90
+
+  # The general NetworkPolicy rules implemented by this chart may be too restrictive
+  # for some setups, e.g. when trying to replicate to a Gerrit replica. Here
+  # custom rules may be added to whitelist some additional connections.
+  networkPolicy:
+    ingress: []
+    egress: []
+    # An example for an egress rule to allow replication to a Gerrit replica
+    # installed with the gerrit-replica setup in the same cluster and namespace
+    # by using the service as the replication destination
+    # (e.g. http://gerrit-replica-git-backend-service:80/git/${name}.git):
+    #
+    # - to:
+    #   - podSelector:
+    #       matchLabels:
+    #         app: git-backend
+
+  service:
+    additionalAnnotations: {}
+    loadBalancerSourceRanges: []
+    type: NodePort
+    externalTrafficPolicy: Cluster
+    http:
+      port: 80
+    ssh:
+      enabled: false
+      port: 29418
+
+  # `gerrit.keystore` expects a base64-encoded Java-keystore
+  # Since Java keystores are binary files, adding the unencoded content and
+  # automatic encoding using helm does not work here.
+  keystore:
+
+  index:
+    # Either `lucene` or `elasticsearch`
+    type: lucene
+
+  pluginManagement:
+    plugins: []
+    # A plugin packaged in the gerrit.war-file
+    # - name: download-commands
+
+    # A plugin packaged in the gerrit.war-file that will also be installed as a
+    # lib
+    # - name: replication
+    #   installAsLibrary: true
+
+    # A plugin that will be downloaded on startup
+    # - name: delete-project
+    #   url: https://example.com/gerrit-plugins/delete-project.jar
+    #   sha1:
+    #   installAsLibrary: false
+
+    # Only downloaded plugins will be cached. This will be ignored, if no plugins
+    # are downloaded.
+    libs: []
+    cache:
+      enabled: false
+      size: 1Gi
+
+  priorityClassName:
+
+  etc:
+    # Some values are expected to have a specific value for the deployment installed
+    # by this chart to work. These are marked with `# FIXED`.
+    # Do not change them!
+    config:
+      gerrit.config: |-
+        [gerrit]
+          basePath = git # FIXED
+          serverId = gerrit-1
+          # The canonical web URL has to be set to the Ingress host, if an Ingress
+          # is used. If a LoadBalancer-service is used, this should be set to the
+          # LoadBalancer's external IP. This can only be done manually after installing
+          # the chart, when you know the external IP the LoadBalancer got from the
+          # cluster.
+          canonicalWebUrl = http://example.com/
+          disableReverseDnsLookup = true
+        [index]
+          type = LUCENE
+        [auth]
+          type = DEVELOPMENT_BECOME_ANY_ACCOUNT
+        [httpd]
+          # If using an ingress use proxy-http or proxy-https
+          listenUrl = proxy-http://*:8080/
+          requestLog = true
+          gracefulStopTimeout = 1m
+        [sshd]
+          listenAddress = off
+        [transfer]
+          timeout = 120 s
+        [user]
+          name = Gerrit Code Review
+          email = gerrit@example.com
+          anonymousCoward = Unnamed User
+        [cache]
+          directory = cache
+        [container]
+          user = gerrit # FIXED
+          javaHome = /usr/lib/jvm/java-11-openjdk # FIXED
+          javaOptions = -Djavax.net.ssl.trustStore=/var/gerrit/etc/keystore # FIXED
+          javaOptions = -Xms200m
+          # Has to be lower than 'gerrit.resources.limits.memory'. Also
+          # consider memories used by other applications in the container.
+          javaOptions = -Xmx4g
+
+      replication.config: |-
+        [gerrit]
+          autoReload = false
+          replicateOnStartup = true
+          defaultForceUpdate = true
+
+        # [remote "replica"]
+        # url = http://gerrit-replica.example.com/git/${name}.git
+        # replicationDelay = 0
+        # timeout = 30
+
+    secret:
+      secure.config: |-
+        # Password for the keystore added as value for 'gerritReplica.keystore'
+        # Only needed, if SSL is enabled.
+        #[httpd]
+        #  sslKeyPassword = gerrit
+
+        # Credentials for replication targets
+        # [remote "replica"]
+        # username = git
+        # password = secret
+
+      # ssh_host_ecdsa_key: |-
+      #   -----BEGIN EC PRIVATE KEY-----
+
+      #   -----END EC PRIVATE KEY-----
+
+      # ssh_host_ecdsa_key.pub: ecdsa-sha2-nistp256...
+
+  additionalConfigMaps:
+    # - name:
+    #   subDir:
+    #   data:
+    #     file.txt: test
diff --git a/charts/k8s-gerrit/istio/README.md b/charts/k8s-gerrit/istio/README.md
new file mode 100644
index 0000000..2f03490
--- /dev/null
+++ b/charts/k8s-gerrit/istio/README.md
@@ -0,0 +1,25 @@
+# Istio for Gerrit
+
+## Configuring istio
+
+It is recommended to set a static IP to be used by the LoadBalancer service
+deployed by istio. To do that set
+`spec.components.ingressGateways[0].k8s.overlays[0].patches[0].value`, which is
+commented out by default, which causes the use of an ephemeral IP.
+
+## Installing istio
+
+Create the `istio-system`-namespace:
+
+```sh
+kubectl apply -f ./istio/istio-system-namespace.yaml
+```
+
+Verify that your istioctl version (`istioctl version`) matches the version in
+`istio/gerrit.profile.yaml` under `spec.tag`.
+
+Install istio:
+
+```sh
+istioctl install -f istio/gerrit.profile.yaml
+```
diff --git a/charts/k8s-gerrit/istio/gerrit.profile.yaml b/charts/k8s-gerrit/istio/gerrit.profile.yaml
new file mode 100644
index 0000000..d81dea6
--- /dev/null
+++ b/charts/k8s-gerrit/istio/gerrit.profile.yaml
@@ -0,0 +1,312 @@
+apiVersion: install.istio.io/v1alpha1
+kind: IstioOperator
+spec:
+  components:
+    base:
+      enabled: true
+    cni:
+      enabled: false
+    egressGateways:
+    - enabled: false
+      k8s:
+        env:
+        - name: ISTIO_META_ROUTER_MODE
+          value: standard
+        hpaSpec:
+          maxReplicas: 5
+          metrics:
+          - resource:
+              name: cpu
+              target:
+                type: Utilization
+                averageUtilization: 80
+            type: Resource
+          minReplicas: 1
+          scaleTargetRef:
+            apiVersion: apps/v1
+            kind: Deployment
+            name: istio-egressgateway
+        resources:
+          limits:
+            cpu: 2000m
+            memory: 1024Mi
+          requests:
+            cpu: 100m
+            memory: 128Mi
+        service:
+          ports:
+          - name: http2
+            port: 80
+            protocol: TCP
+            targetPort: 8080
+          - name: https
+            port: 443
+            protocol: TCP
+            targetPort: 8443
+          - name: tls
+            port: 15443
+            protocol: TCP
+            targetPort: 15443
+        strategy:
+          rollingUpdate:
+            maxSurge: 100%
+            maxUnavailable: 25%
+      name: istio-egressgateway
+    ingressGateways:
+    - enabled: true
+      k8s:
+        env:
+        - name: ISTIO_META_ROUTER_MODE
+          value: standard
+        hpaSpec:
+          maxReplicas: 5
+          metrics:
+          - resource:
+              name: cpu
+              target:
+                type: Utilization
+                averageUtilization: 80
+            type: Resource
+          minReplicas: 5
+          scaleTargetRef:
+            apiVersion: apps/v1
+            kind: Deployment
+            name: istio-ingressgateway
+        resources:
+          limits:
+            cpu: 2000m
+            memory: 1024Mi
+          requests:
+            cpu: 100m
+            memory: 128Mi
+        service:
+          ports:
+          - name: status-port
+            port: 15021
+            protocol: TCP
+            targetPort: 15021
+          - name: http2
+            port: 80
+            protocol: TCP
+            targetPort: 8080
+          - name: https
+            port: 443
+            protocol: TCP
+            targetPort: 8443
+          - name: tcp-istiod
+            port: 15012
+            protocol: TCP
+            targetPort: 15012
+          # - name: tls
+          #   port: 15443
+          #   protocol: TCP
+          #   targetPort: 15443
+          - name: ssh
+            port: 29418
+            protocol: TCP
+            targetPort: 29418
+        strategy:
+          rollingUpdate:
+            maxSurge: 100%
+            maxUnavailable: 25%
+        overlays:
+          - kind: Service
+            name: istio-ingressgateway
+            patches:
+              - path: spec.loadBalancerIP
+                # TO_BE_CHANGED: Change IP
+                #value: xxx.xxx.xxx.xxx
+              - path: spec.loadBalancerSourceRanges
+                # TO_BE_CHANGED: Change IP-Range to whitelist
+                # value:
+                # - 0.0.0.0/32
+              - path: metadata.annotations
+                # TO_BE_CHANGED: Annotations to be set in the service, e.g. to
+                # configure automated DNS and certificate management in Gardener
+                # value:
+                #   dns.gardener.cloud/dnsnames: '*.example.com'
+                #   dns.gardener.cloud/class: garden
+                #   dns.gardener.cloud/ttl: "600"
+                #   cert.gardener.cloud/commonName: '*.example.com'
+                #   cert.gardener.cloud/purpose: managed
+                #   cert.gardener.cloud/secretname: tls-secret
+      name: istio-ingressgateway
+    istiodRemote:
+      enabled: false
+    pilot:
+      enabled: true
+      k8s:
+        env:
+        - name: POD_NAME
+          valueFrom:
+            fieldRef:
+              apiVersion: v1
+              fieldPath: metadata.name
+        - name: POD_NAMESPACE
+          valueFrom:
+            fieldRef:
+              apiVersion: v1
+              fieldPath: metadata.namespace
+        hpaSpec:
+          minReplicas: 2
+        readinessProbe:
+          httpGet:
+            path: /ready
+            port: 8080
+          initialDelaySeconds: 1
+          periodSeconds: 3
+          timeoutSeconds: 5
+        strategy:
+          rollingUpdate:
+            maxSurge: 100%
+            maxUnavailable: 50%
+  hub: docker.io/istio
+  meshConfig:
+    accessLogFile: /dev/stdout
+    defaultConfig:
+      proxyMetadata: {}
+    enablePrometheusMerge: true
+  profile: default
+  tag: 1.16.0
+  values:
+    base:
+      enableCRDTemplates: false
+      validationURL: ""
+    gateways:
+      istio-egressgateway:
+        autoscaleEnabled: true
+        env: {}
+        name: istio-egressgateway
+        secretVolumes:
+        - mountPath: /etc/istio/egressgateway-certs
+          name: egressgateway-certs
+          secretName: istio-egressgateway-certs
+        - mountPath: /etc/istio/egressgateway-ca-certs
+          name: egressgateway-ca-certs
+          secretName: istio-egressgateway-ca-certs
+        type: ClusterIP
+      istio-ingressgateway:
+        autoscaleEnabled: true
+        env: {}
+        name: istio-ingressgateway
+        secretVolumes:
+        - mountPath: /etc/istio/ingressgateway-certs
+          name: ingressgateway-certs
+          secretName: istio-ingressgateway-certs
+        - mountPath: /etc/istio/ingressgateway-ca-certs
+          name: ingressgateway-ca-certs
+          secretName: istio-ingressgateway-ca-certs
+        type: LoadBalancer
+    global:
+      configValidation: true
+      defaultNodeSelector: {}
+      defaultPodDisruptionBudget:
+        enabled: true
+      defaultResources:
+        requests:
+          cpu: 10m
+      imagePullPolicy: ""
+      imagePullSecrets: []
+      istioNamespace: istio-system
+      istiod:
+        enableAnalysis: false
+      jwtPolicy: third-party-jwt
+      logAsJson: false
+      logging:
+        level: default:info
+      meshNetworks: {}
+      mountMtlsCerts: false
+      multiCluster:
+        clusterName: ""
+        enabled: false
+      network: ""
+      omitSidecarInjectorConfigMap: false
+      oneNamespace: false
+      operatorManageWebhooks: false
+      pilotCertProvider: istiod
+      priorityClassName: ""
+      proxy:
+        autoInject: enabled
+        clusterDomain: cluster.local
+        componentLogLevel: misc:error
+        enableCoreDump: false
+        excludeIPRanges: ""
+        excludeInboundPorts: ""
+        excludeOutboundPorts: ""
+        image: proxyv2
+        includeIPRanges: '*'
+        # Use this value, if more detailed logging output is needed, e.g. for
+        # debugging.
+        logLevel: warning
+        privileged: false
+        readinessFailureThreshold: 30
+        readinessInitialDelaySeconds: 1
+        readinessPeriodSeconds: 2
+        resources:
+          limits:
+            cpu: 2000m
+            memory: 1024Mi
+          requests:
+            cpu: 100m
+            memory: 128Mi
+        statusPort: 15020
+        tracer: zipkin
+      proxy_init:
+        image: proxyv2
+        resources:
+          limits:
+            cpu: 2000m
+            memory: 1024Mi
+          requests:
+            cpu: 10m
+            memory: 10Mi
+      sds:
+        token:
+          aud: istio-ca
+      sts:
+        servicePort: 0
+      tracer:
+        datadog: {}
+        lightstep: {}
+        stackdriver: {}
+        zipkin: {}
+      useMCP: false
+    istiodRemote:
+      injectionURL: ""
+    pilot:
+      autoscaleEnabled: true
+      autoscaleMax: 5
+      autoscaleMin: 2
+      configMap: true
+      cpu:
+        targetAverageUtilization: 80
+      enableProtocolSniffingForInbound: true
+      enableProtocolSniffingForOutbound: true
+      env: {}
+      image: pilot
+      keepaliveMaxServerConnectionAge: 24h
+      nodeSelector: {}
+      podLabels: {}
+      replicaCount: 1
+      traceSampling: 1
+    sidecarInjectorWebhook:
+      enableNamespacesByDefault: false
+      objectSelector:
+        autoInject: true
+        enabled: false
+      rewriteAppHTTPProbe: true
+    telemetry:
+      enabled: true
+      v2:
+        enabled: true
+        metadataExchange:
+          wasmEnabled: false
+        prometheus:
+          enabled: true
+          wasmEnabled: false
+        stackdriver:
+          configOverride: {}
+          enabled: false
+          logging: false
+          monitoring: false
+          topology: false
diff --git a/charts/k8s-gerrit/istio/istio-system-namespace.yaml b/charts/k8s-gerrit/istio/istio-system-namespace.yaml
new file mode 100644
index 0000000..f394e91
--- /dev/null
+++ b/charts/k8s-gerrit/istio/istio-system-namespace.yaml
@@ -0,0 +1,4 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: istio-system
diff --git a/charts/k8s-gerrit/istio/namespace.yaml b/charts/k8s-gerrit/istio/namespace.yaml
new file mode 100644
index 0000000..6e9fb38
--- /dev/null
+++ b/charts/k8s-gerrit/istio/namespace.yaml
@@ -0,0 +1,6 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: gerrit-replica
+  labels:
+    istio-injection: enabled
diff --git a/charts/k8s-gerrit/operator/.gitignore b/charts/k8s-gerrit/operator/.gitignore
new file mode 100644
index 0000000..7021fd2
--- /dev/null
+++ b/charts/k8s-gerrit/operator/.gitignore
@@ -0,0 +1,4 @@
+.classpath
+.project
+.settings/
+/target/
diff --git a/charts/k8s-gerrit/operator/README.md b/charts/k8s-gerrit/operator/README.md
new file mode 100644
index 0000000..40db2fd
--- /dev/null
+++ b/charts/k8s-gerrit/operator/README.md
@@ -0,0 +1,4 @@
+# Gerrit Operator
+
+Detailed documentation about the Gerrit operator can be found
+[here](../Documentation/operator.md).
diff --git a/charts/k8s-gerrit/operator/k8s/operator/namespace.yaml b/charts/k8s-gerrit/operator/k8s/operator/namespace.yaml
new file mode 100644
index 0000000..9ce8374
--- /dev/null
+++ b/charts/k8s-gerrit/operator/k8s/operator/namespace.yaml
@@ -0,0 +1,4 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: gerrit-operator
diff --git a/charts/k8s-gerrit/operator/k8s/operator/operator.yaml b/charts/k8s-gerrit/operator/k8s/operator/operator.yaml
new file mode 100644
index 0000000..02c6ef6
--- /dev/null
+++ b/charts/k8s-gerrit/operator/k8s/operator/operator.yaml
@@ -0,0 +1,63 @@
+## Required to use an external/persistent keystore, otherwise a keystore using
+## self-signed certificates will be generated
+# ---
+# apiVersion: v1
+# kind: Secret
+# metadata:
+#   name:  gerrit-operator-ssl
+#   namespace: gerrit-operator
+# data:
+#   keystore.jks: # base64-encoded Java keystore
+#   keystore.password: # base64-encoded Java keystore password
+# type: Opaque
+
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: gerrit-operator
+  namespace: gerrit-operator
+spec:
+  selector:
+    matchLabels:
+      app: gerrit-operator
+  template:
+    metadata:
+      labels:
+        app: gerrit-operator
+    spec:
+      serviceAccountName: gerrit-operator
+      containers:
+      - name: operator
+        image: k8sgerrit/gerrit-operator
+        imagePullPolicy: Always
+        env:
+        - name: NAMESPACE
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.namespace
+        - name: INGRESS
+          value: none
+        ports:
+        - containerPort: 80
+        readinessProbe:
+          httpGet:
+            path: /health
+            port: 8080
+            scheme: HTTPS
+          initialDelaySeconds: 1
+        livenessProbe:
+          httpGet:
+            path: /health
+            port: 8080
+            scheme: HTTPS
+          initialDelaySeconds: 30
+      ## Only required, if an external/persistent keystore is being used.
+      #   volumeMounts:
+      #   - name: ssl
+      #     readOnly: true
+      #     mountPath: /operator
+      # volumes:
+      # - name: ssl
+      #   secret:
+      #     secretName: gerrit-operator-ssl
diff --git a/charts/k8s-gerrit/operator/k8s/operator/rbac.yaml b/charts/k8s-gerrit/operator/k8s/operator/rbac.yaml
new file mode 100644
index 0000000..201cce7
--- /dev/null
+++ b/charts/k8s-gerrit/operator/k8s/operator/rbac.yaml
@@ -0,0 +1,87 @@
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: gerrit-operator
+  namespace: gerrit-operator
+
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  name: gerrit-operator-admin
+subjects:
+- kind: ServiceAccount
+  name: gerrit-operator
+  namespace: gerrit-operator
+roleRef:
+  kind: ClusterRole
+  name: gerrit-operator
+  apiGroup: ""
+
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: gerrit-operator
+rules:
+- apiGroups:
+  - "batch"
+  resources:
+  - cronjobs
+  verbs:
+  - '*'
+- apiGroups:
+  - "apps"
+  resources:
+  - statefulsets
+  - deployments
+  verbs:
+  - '*'
+- apiGroups:
+  - ""
+  resources:
+  - configmaps
+  - persistentvolumeclaims
+  - secrets
+  - services
+  verbs:
+  - '*'
+- apiGroups:
+  - "storage.k8s.io"
+  resources:
+  - storageclasses
+  verbs:
+  - 'get'
+  - 'list'
+- apiGroups:
+  - "apiextensions.k8s.io"
+  resources:
+  - customresourcedefinitions
+  verbs:
+  - '*'
+- apiGroups:
+  - "networking.k8s.io"
+  resources:
+  - ingresses
+  verbs:
+  - '*'
+- apiGroups:
+  - "gerritoperator.google.com"
+  resources:
+  - '*'
+  verbs:
+  - '*'
+- apiGroups:
+  - "networking.istio.io"
+  resources:
+  - "gateways"
+  - "virtualservices"
+  - "destinationrules"
+  verbs:
+  - '*'
+- apiGroups:
+  - "admissionregistration.k8s.io"
+  resources:
+  - 'validatingwebhookconfigurations'
+  verbs:
+  - '*'
diff --git a/charts/k8s-gerrit/operator/k8s/resources/rbac.yaml b/charts/k8s-gerrit/operator/k8s/resources/rbac.yaml
new file mode 100644
index 0000000..a31f4a7
--- /dev/null
+++ b/charts/k8s-gerrit/operator/k8s/resources/rbac.yaml
@@ -0,0 +1,30 @@
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: gerrit
+  namespace: gerrit #CHANGE: Change it to the namespace running Gerrit
+
+---
+kind: ClusterRole
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: gerrit
+rules:
+- apiGroups: [""]
+  resources: ["pods"]
+  verbs: ["get", "list"]
+
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  name: gerrit
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: gerrit
+subjects:
+- kind: ServiceAccount
+  name: gerrit
+  namespace: gerrit #CHANGE: Change it to the namespace running Gerrit
diff --git a/charts/k8s-gerrit/operator/pom.xml b/charts/k8s-gerrit/operator/pom.xml
new file mode 100644
index 0000000..890c400
--- /dev/null
+++ b/charts/k8s-gerrit/operator/pom.xml
@@ -0,0 +1,398 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+
+	<groupId>com.google.gerrit.operator</groupId>
+	<artifactId>operator</artifactId>
+	<version>${revision}</version>
+
+	<name>Gerrit Kubernetes Operator</name>
+	<description>Provisions and operates Gerrit instances in Kubernetes</description>
+	<packaging>jar</packaging>
+
+	<properties>
+		<revision>1.0.0-SNAPSHOT</revision>
+
+		<fabric8.version>6.6.2</fabric8.version>
+		<flogger.version>0.7.4</flogger.version>
+		<guice.version>5.1.0</guice.version>
+		<javaoperatorsdk.version>4.3.3</javaoperatorsdk.version>
+		<jetty.version>11.0.15</jetty.version>
+		<lombok.version>1.18.28</lombok.version>
+		<maven.compiler.source>11</maven.compiler.source>
+		<maven.compiler.target>11</maven.compiler.target>
+		<docker.registry>docker.io</docker.registry>
+		<docker.org>k8sgerrit</docker.org>
+
+		<test.docker.registry>docker.io</test.docker.registry>
+		<test.docker.org>k8sgerritdev</test.docker.org>
+	</properties>
+
+	<profiles>
+		<profile>
+			<id>publish</id>
+			<build>
+				<plugins>
+					<plugin>
+						<groupId>com.google.cloud.tools</groupId>
+						<artifactId>jib-maven-plugin</artifactId>
+						<version>3.3.1</version>
+						<executions>
+							<execution>
+								<phase>package</phase>
+								<goals>
+									<goal>build</goal>
+								</goals>
+								<configuration>
+									<container>
+										<mainClass>com.google.gerrit.k8s.operator.Main</mainClass>
+									</container>
+									<containerizingMode>packaged</containerizingMode>
+									<from>
+										<image>gcr.io/distroless/java:11</image>
+									</from>
+									<to>
+										<image>${docker.registry}/${docker.org}/gerrit-operator</image>
+										<tags>
+											<tag>${project.version}</tag>
+										</tags>
+									</to>
+								</configuration>
+							</execution>
+						</executions>
+					</plugin>
+				</plugins>
+			</build>
+		</profile>
+		<profile>
+			<id>integration-test</id>
+			<build>
+				<plugins>
+					<plugin>
+						<groupId>org.codehaus.mojo</groupId>
+						<artifactId>properties-maven-plugin</artifactId>
+						<version>1.1.0</version>
+						<executions>
+							<execution>
+								<phase>initialize</phase>
+								<goals>
+									<goal>read-project-properties</goal>
+								</goals>
+								<configuration>
+									<files>
+										<file>${basedir}/test.properties</file>
+									</files>
+								</configuration>
+							</execution>
+						</executions>
+					</plugin>
+					<plugin>
+						<groupId>com.google.cloud.tools</groupId>
+						<artifactId>jib-maven-plugin</artifactId>
+						<version>3.3.1</version>
+						<executions>
+							<execution>
+								<phase>pre-integration-test</phase>
+								<goals>
+									<goal>build</goal>
+								</goals>
+								<configuration>
+									<container>
+										<mainClass>com.google.gerrit.k8s.operator.Main</mainClass>
+									</container>
+									<containerizingMode>packaged</containerizingMode>
+									<from>
+										<image>gcr.io/distroless/java:11</image>
+									</from>
+									<to>
+										<image>
+											${test.docker.registry}/${test.docker.org}/gerrit-operator</image>
+										<tags>
+											<tag>${project.version}</tag>
+										</tags>
+									</to>
+								</configuration>
+							</execution>
+						</executions>
+					</plugin>
+					<plugin>
+						<artifactId>maven-failsafe-plugin</artifactId>
+						<version>2.22.2</version>
+						<executions>
+							<execution>
+								<phase>integration-test</phase>
+								<goals>
+									<goal>integration-test</goal>
+									<goal>verify</goal>
+								</goals>
+								<configuration>
+									<includes>
+										<include>**/*E2E.java</include>
+									</includes>
+								</configuration>
+							</execution>
+						</executions>
+					</plugin>
+				</plugins>
+			</build>
+		</profile>
+	</profiles>
+
+	<dependencies>
+		<dependency>
+			<groupId>io.javaoperatorsdk</groupId>
+			<artifactId>operator-framework</artifactId>
+			<version>${javaoperatorsdk.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>io.javaoperatorsdk</groupId>
+			<artifactId>micrometer-support</artifactId>
+			<version>${javaoperatorsdk.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>io.javaoperatorsdk</groupId>
+			<artifactId>operator-framework-junit-5</artifactId>
+			<version>${javaoperatorsdk.version}</version>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>io.fabric8</groupId>
+			<artifactId>kubernetes-client</artifactId>
+			<version>${fabric8.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>io.fabric8</groupId>
+			<artifactId>istio-client</artifactId>
+			<version>${fabric8.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>io.fabric8</groupId>
+			<artifactId>crd-generator-apt</artifactId>
+			<version>${fabric8.version}</version>
+			<scope>provided</scope>
+		</dependency>
+		<dependency>
+			<groupId>io.fabric8</groupId>
+			<artifactId>generator-annotations</artifactId>
+			<version>${fabric8.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.projectlombok</groupId>
+			<artifactId>lombok</artifactId>
+			<scope>provided</scope>
+			<version>${lombok.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.eclipse.jetty</groupId>
+			<artifactId>jetty-server</artifactId>
+			<version>${jetty.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.eclipse.jetty</groupId>
+			<artifactId>jetty-servlet</artifactId>
+			<version>${jetty.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>com.google.flogger</groupId>
+			<artifactId>flogger</artifactId>
+			<version>${flogger.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>com.google.flogger</groupId>
+			<artifactId>flogger-log4j2-backend</artifactId>
+			<version>${flogger.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>com.google.inject</groupId>
+			<artifactId>guice</artifactId>
+			<version>${guice.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>com.google.inject.extensions</groupId>
+			<artifactId>guice-assistedinject</artifactId>
+			<version>${guice.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.logging.log4j</groupId>
+			<artifactId>log4j-slf4j-impl</artifactId>
+			<version>2.19.0</version>
+		</dependency>
+		<dependency>
+			<groupId>org.eclipse.jgit</groupId>
+			<artifactId>org.eclipse.jgit</artifactId>
+			<version>6.5.0.202303070854-r</version>
+		</dependency>
+		<dependency>
+			<groupId>org.bouncycastle</groupId>
+			<artifactId>bcpkix-jdk18on</artifactId>
+			<version>1.73</version>
+		</dependency>
+		<dependency>
+			<groupId>com.urswolfer.gerrit.client.rest</groupId>
+			<artifactId>gerrit-rest-java-client</artifactId>
+			<version>0.9.5</version>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.mockito</groupId>
+			<artifactId>mockito-core</artifactId>
+			<version>4.8.0</version>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>io.fabric8</groupId>
+			<artifactId>kubernetes-server-mock</artifactId>
+			<version>${fabric8.version}</version>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>com.google.truth</groupId>
+			<artifactId>truth</artifactId>
+			<version>0.32</version>
+		</dependency>
+		<dependency>
+			<groupId>org.junit.jupiter</groupId>
+			<artifactId>junit-jupiter-params</artifactId>
+			<version>5.9.2</version>
+			<scope>test</scope>
+		</dependency>
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>io.fabric8</groupId>
+				<artifactId>java-generator-maven-plugin</artifactId>
+				<version>${fabric8.version}</version>
+				<configuration>
+					<source>${project.basedir}/src/main/resources/crd/emissary-crds.yaml</source>
+					<!-- Generate sundrio @Buildable annotations that generate Builder classes-->
+					<extraAnnotations>true</extraAnnotations>
+				</configuration>
+				<executions>
+					<execution>
+						<goals>
+							<goal>generate</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+			<plugin>
+				<groupId>com.spotify.fmt</groupId>
+				<artifactId>fmt-maven-plugin</artifactId>
+				<version>2.19</version>
+				<executions>
+					<execution>
+						<goals>
+							<goal>format</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-jar-plugin</artifactId>
+				<version>3.2.2</version>
+				<configuration>
+					<archive>
+						<manifest>
+							<mainClass>com.google.gerrit.k8s.operator.Main</mainClass>
+							<addDefaultImplementationEntries>
+								true
+							</addDefaultImplementationEntries>
+						</manifest>
+					</archive>
+				</configuration>
+			</plugin>
+			<plugin>
+				<groupId>com.google.cloud.tools</groupId>
+				<artifactId>jib-maven-plugin</artifactId>
+				<version>3.3.1</version>
+				<executions>
+					<execution>
+						<phase>package</phase>
+						<goals>
+							<goal>dockerBuild</goal>
+						</goals>
+						<configuration>
+							<container>
+								<mainClass>com.google.gerrit.k8s.operator.Main</mainClass>
+							</container>
+							<containerizingMode>packaged</containerizingMode>
+							<from>
+								<image>gcr.io/distroless/java:11</image>
+							</from>
+							<to>
+								<image>gerrit-operator</image>
+								<tags>${revision}</tags>
+							</to>
+						</configuration>
+					</execution>
+				</executions>
+			</plugin>
+			<plugin>
+				<artifactId>maven-resources-plugin</artifactId>
+				<version>3.3.1</version>
+				<executions>
+					<execution>
+						<id>copy-crds</id>
+						<phase>package</phase>
+						<goals>
+							<goal>copy-resources</goal>
+						</goals>
+						<configuration>
+							<outputDirectory>../helm-charts/gerrit-operator-crds/templates</outputDirectory>
+							<resources>
+								<resource>
+									<directory>target/classes/META-INF/fabric8</directory>
+									<includes>
+										<include>*-v1.yml</include>
+									</includes>
+								</resource>
+							</resources>
+						</configuration>
+					</execution>
+				</executions>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-compiler-plugin</artifactId>
+				<version>3.10.0</version>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-surefire-plugin</artifactId>
+				<version>2.22.2</version>
+				<configuration>
+					<includes>
+						<include>**/*Test.java</include>
+					</includes>
+					<rerunFailingTestsCount>1</rerunFailingTestsCount>
+				</configuration>
+			</plugin>
+			<plugin>
+				<groupId>org.codehaus.mojo</groupId>
+				<artifactId>build-helper-maven-plugin</artifactId>
+				<version>3.4.0</version>
+				<executions>
+					<execution>
+						<id>add-source</id>
+						<phase>generate-sources</phase>
+						<goals>
+							<goal>add-source</goal>
+						</goals>
+						<configuration>
+							<sources>
+								<source>${project.build.directory}/generated-sources/annotations/</source>
+								<source>${project.build.directory}/generated-sources/java/</source>
+								<source>${project.build.directory}/generated-test-sources/java/</source>
+							</sources>
+						</configuration>
+					</execution>
+				</executions>
+			</plugin>
+		</plugins>
+	</build>
+</project>
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/Constants.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/Constants.java
new file mode 100644
index 0000000..d2fb405
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/Constants.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator;
+
+import com.google.inject.AbstractModule;
+
+public class Constants extends AbstractModule {
+  public static final String[] VERSIONS = new String[] {"v1alpha"};
+  public static final String[] CUSTOM_RESOURCES =
+      new String[] {"GerritCluster", "Gerrit", "Receiver", "GerritNetwork", "GitGarbageCollection"};
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/EnvModule.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/EnvModule.java
new file mode 100644
index 0000000..1f5b04c
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/EnvModule.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator;
+
+import com.google.gerrit.k8s.operator.network.IngressType;
+import com.google.inject.AbstractModule;
+import com.google.inject.name.Names;
+
+public class EnvModule extends AbstractModule {
+  @Override
+  protected void configure() {
+    bind(String.class)
+        .annotatedWith(Names.named("Namespace"))
+        .toInstance(System.getenv("NAMESPACE"));
+
+    String ingressTypeEnv = System.getenv("INGRESS");
+    IngressType ingressType =
+        ingressTypeEnv == null
+            ? IngressType.NONE
+            : IngressType.valueOf(ingressTypeEnv.toUpperCase());
+    bind(IngressType.class).annotatedWith(Names.named("IngressType")).toInstance(ingressType);
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/GerritOperator.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/GerritOperator.java
new file mode 100644
index 0000000..6f9be7e
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/GerritOperator.java
@@ -0,0 +1,111 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator;
+
+import static com.google.gerrit.k8s.operator.server.HttpServer.PORT;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+import io.fabric8.kubernetes.api.model.Service;
+import io.fabric8.kubernetes.api.model.ServiceBuilder;
+import io.fabric8.kubernetes.api.model.ServicePort;
+import io.fabric8.kubernetes.api.model.ServicePortBuilder;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import io.javaoperatorsdk.operator.Operator;
+import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
+import java.util.Map;
+import java.util.Set;
+
+@Singleton
+public class GerritOperator {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  public static final String SERVICE_NAME = "gerrit-operator";
+  public static final int SERVICE_PORT = 8080;
+
+  private final KubernetesClient client;
+  private final LifecycleManager lifecycleManager;
+
+  @SuppressWarnings("rawtypes")
+  private final Set<Reconciler> reconcilers;
+
+  private final String namespace;
+
+  private Operator operator;
+  private Service svc;
+
+  @Inject
+  @SuppressWarnings("rawtypes")
+  public GerritOperator(
+      LifecycleManager lifecycleManager,
+      KubernetesClient client,
+      Set<Reconciler> reconcilers,
+      @Named("Namespace") String namespace) {
+    this.lifecycleManager = lifecycleManager;
+    this.client = client;
+    this.reconcilers = reconcilers;
+    this.namespace = namespace;
+  }
+
+  public void start() throws Exception {
+    operator = new Operator(client);
+    for (Reconciler<?> reconciler : reconcilers) {
+      logger.atInfo().log(
+          String.format("Registering reconciler: %s", reconciler.getClass().getSimpleName()));
+      operator.register(reconciler);
+    }
+    operator.start();
+    lifecycleManager.addShutdownHook(
+        new Runnable() {
+          @Override
+          public void run() {
+            shutdown();
+          }
+        });
+    applyService();
+  }
+
+  public void shutdown() {
+    client.resource(svc).delete();
+    operator.stop();
+  }
+
+  private void applyService() {
+    ServicePort port =
+        new ServicePortBuilder()
+            .withName("http")
+            .withPort(SERVICE_PORT)
+            .withNewTargetPort(PORT)
+            .withProtocol("TCP")
+            .build();
+    svc =
+        new ServiceBuilder()
+            .withApiVersion("v1")
+            .withNewMetadata()
+            .withName(SERVICE_NAME)
+            .withNamespace(namespace)
+            .endMetadata()
+            .withNewSpec()
+            .withType("ClusterIP")
+            .withPorts(port)
+            .withSelector(Map.of("app", "gerrit-operator"))
+            .endSpec()
+            .build();
+
+    logger.atInfo().log(String.format("Applying Service for Gerrit Operator: %s", svc.toString()));
+    client.resource(svc).createOrReplace();
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/LifecycleManager.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/LifecycleManager.java
new file mode 100644
index 0000000..8e556a7
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/LifecycleManager.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator;
+
+import com.google.common.collect.Lists;
+import com.google.inject.Singleton;
+import java.util.ArrayList;
+import java.util.List;
+
+@Singleton
+public class LifecycleManager {
+  private List<Runnable> shutdownHooks = new ArrayList<>();
+
+  public LifecycleManager() {
+    Runtime.getRuntime().addShutdownHook(new Thread(this::executeShutdownHooks));
+  }
+
+  public void addShutdownHook(Runnable hook) {
+    shutdownHooks.add(hook);
+  }
+
+  private void executeShutdownHooks() {
+    for (Runnable hook : Lists.reverse(shutdownHooks)) {
+      hook.run();
+    }
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/Main.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/Main.java
new file mode 100644
index 0000000..8fc1428
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/Main.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator;
+
+import com.google.gerrit.k8s.operator.admission.ValidationWebhookConfigs;
+import com.google.gerrit.k8s.operator.server.HttpServer;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Stage;
+
+public class Main {
+
+  public static void main(String[] args) throws Exception {
+    Injector injector = Guice.createInjector(Stage.PRODUCTION, new OperatorModule());
+    injector.getInstance(HttpServer.class).start();
+    injector.getInstance(ValidationWebhookConfigs.class).apply();
+    injector.getInstance(GerritOperator.class).start();
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/OperatorModule.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/OperatorModule.java
new file mode 100644
index 0000000..3e61528
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/OperatorModule.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator;
+
+import com.google.gerrit.k8s.operator.admission.AdmissionWebhookModule;
+import com.google.gerrit.k8s.operator.cluster.GerritClusterReconciler;
+import com.google.gerrit.k8s.operator.gerrit.GerritReconciler;
+import com.google.gerrit.k8s.operator.gitgc.GitGarbageCollectionReconciler;
+import com.google.gerrit.k8s.operator.network.GerritNetworkReconcilerProvider;
+import com.google.gerrit.k8s.operator.receiver.ReceiverReconciler;
+import com.google.gerrit.k8s.operator.server.ServerModule;
+import com.google.inject.AbstractModule;
+import com.google.inject.multibindings.Multibinder;
+import io.fabric8.kubernetes.client.Config;
+import io.fabric8.kubernetes.client.ConfigBuilder;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import io.fabric8.kubernetes.client.KubernetesClientBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
+
+public class OperatorModule extends AbstractModule {
+  @SuppressWarnings("rawtypes")
+  @Override
+  protected void configure() {
+    install(new EnvModule());
+    install(new ServerModule());
+
+    bind(KubernetesClient.class).toInstance(getKubernetesClient());
+    bind(LifecycleManager.class);
+    bind(GerritOperator.class);
+
+    install(new AdmissionWebhookModule());
+
+    Multibinder<Reconciler> reconcilers = Multibinder.newSetBinder(binder(), Reconciler.class);
+    reconcilers.addBinding().to(GerritClusterReconciler.class);
+    reconcilers.addBinding().to(GerritReconciler.class);
+    reconcilers.addBinding().to(GitGarbageCollectionReconciler.class);
+    reconcilers.addBinding().to(ReceiverReconciler.class);
+    reconcilers.addBinding().toProvider(GerritNetworkReconcilerProvider.class);
+  }
+
+  private KubernetesClient getKubernetesClient() {
+    Config config = new ConfigBuilder().withNamespace(null).build();
+    return new KubernetesClientBuilder().withConfig(config).build();
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/admission/AdmissionWebhookModule.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/admission/AdmissionWebhookModule.java
new file mode 100644
index 0000000..d3d4841
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/admission/AdmissionWebhookModule.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.admission;
+
+import com.google.gerrit.k8s.operator.v1alpha.admission.GerritClusterValidationWebhookConfigApplier;
+import com.google.gerrit.k8s.operator.v1alpha.admission.GerritValidationWebhookConfigApplier;
+import com.google.gerrit.k8s.operator.v1alpha.admission.GitGcValidationWebhookConfigApplier;
+import com.google.inject.AbstractModule;
+import com.google.inject.assistedinject.FactoryModuleBuilder;
+
+public class AdmissionWebhookModule extends AbstractModule {
+  public void configure() {
+    install(new FactoryModuleBuilder().build(ValidationWebhookConfigApplier.Factory.class));
+
+    bind(ValidationWebhookConfigs.class);
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/admission/ValidationWebhookConfigApplier.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/admission/ValidationWebhookConfigApplier.java
new file mode 100644
index 0000000..443347e
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/admission/ValidationWebhookConfigApplier.java
@@ -0,0 +1,144 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.admission;
+
+import static com.google.gerrit.k8s.operator.GerritOperator.SERVICE_NAME;
+import static com.google.gerrit.k8s.operator.GerritOperator.SERVICE_PORT;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.k8s.operator.server.KeyStoreProvider;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+import com.google.inject.name.Named;
+import io.fabric8.kubernetes.api.model.admissionregistration.v1.RuleWithOperations;
+import io.fabric8.kubernetes.api.model.admissionregistration.v1.RuleWithOperationsBuilder;
+import io.fabric8.kubernetes.api.model.admissionregistration.v1.ValidatingWebhook;
+import io.fabric8.kubernetes.api.model.admissionregistration.v1.ValidatingWebhookBuilder;
+import io.fabric8.kubernetes.api.model.admissionregistration.v1.ValidatingWebhookConfiguration;
+import io.fabric8.kubernetes.api.model.admissionregistration.v1.ValidatingWebhookConfigurationBuilder;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import java.io.IOException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.List;
+
+public class ValidationWebhookConfigApplier {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final KubernetesClient client;
+  private final String namespace;
+  private final KeyStoreProvider keyStoreProvider;
+  private final ValidatingWebhookConfiguration cfg;
+  private final String customResourceName;
+  private final String[] customResourceVersions;
+
+  public interface Factory {
+    ValidationWebhookConfigApplier create(
+        String customResourceName, String[] customResourceVersions);
+  }
+
+  @AssistedInject
+  ValidationWebhookConfigApplier(
+      KubernetesClient client,
+      @Named("Namespace") String namespace,
+      KeyStoreProvider keyStoreProvider,
+      @Assisted String customResourceName,
+      @Assisted String[] customResourceVersions) {
+    this.client = client;
+    this.namespace = namespace;
+    this.keyStoreProvider = keyStoreProvider;
+    this.customResourceName = customResourceName;
+    this.customResourceVersions = customResourceVersions;
+
+    this.cfg = build();
+  }
+
+  public List<RuleWithOperations> rules(String version) {
+    return List.of(
+        new RuleWithOperationsBuilder()
+            .withApiGroups("gerritoperator.google.com")
+            .withApiVersions(version)
+            .withOperations("CREATE", "UPDATE")
+            .withResources(customResourceName)
+            .withScope("*")
+            .build());
+  }
+
+  public List<ValidatingWebhook> webhooks()
+      throws CertificateEncodingException, KeyStoreException, NoSuchAlgorithmException,
+          CertificateException, IOException {
+    List<ValidatingWebhook> webhooks = new ArrayList<>();
+    for (String version : customResourceVersions) {
+      webhooks.add(
+          new ValidatingWebhookBuilder()
+              .withName(customResourceName.toLowerCase() + "." + version + ".validator.google.com")
+              .withAdmissionReviewVersions("v1", "v1beta1")
+              .withNewClientConfig()
+              .withCaBundle(caBundle())
+              .withNewService()
+              .withName(SERVICE_NAME)
+              .withNamespace(namespace)
+              .withPath(
+                  String.format("/admission/%s/%s", version, customResourceName).toLowerCase())
+              .withPort(SERVICE_PORT)
+              .endService()
+              .endClientConfig()
+              .withFailurePolicy("Fail")
+              .withMatchPolicy("Equivalent")
+              .withRules(rules(version))
+              .withTimeoutSeconds(10)
+              .withSideEffects("None")
+              .build());
+    }
+    return webhooks;
+  }
+
+  private String caBundle()
+      throws CertificateEncodingException, KeyStoreException, NoSuchAlgorithmException,
+          CertificateException, IOException {
+    return Base64.getEncoder().encodeToString(keyStoreProvider.getCertificate().getBytes());
+  }
+
+  public ValidatingWebhookConfiguration build() {
+    try {
+      return new ValidatingWebhookConfigurationBuilder()
+          .withNewMetadata()
+          .withName(customResourceName.toLowerCase())
+          .endMetadata()
+          .withWebhooks(webhooks())
+          .build();
+    } catch (CertificateException | IOException | KeyStoreException | NoSuchAlgorithmException e) {
+      throw new RuntimeException(
+          "Failed to deploy ValidationWebhookConfiguration " + customResourceName, e);
+    }
+  }
+
+  public void apply()
+      throws KeyStoreException, NoSuchProviderException, IOException, NoSuchAlgorithmException,
+          CertificateException {
+    logger.atInfo().log("Applying webhook config %s", cfg);
+    client.resource(cfg).createOrReplace();
+  }
+
+  public void delete() {
+    logger.atInfo().log("Deleting webhook config %s", cfg);
+    client.resource(cfg).delete();
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/admission/ValidationWebhookConfigs.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/admission/ValidationWebhookConfigs.java
new file mode 100644
index 0000000..901d15f
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/admission/ValidationWebhookConfigs.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.admission;
+
+import static com.google.gerrit.k8s.operator.Constants.CUSTOM_RESOURCES;
+import static com.google.gerrit.k8s.operator.Constants.VERSIONS;
+
+import com.google.gerrit.k8s.operator.LifecycleManager;
+import com.google.inject.Inject;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ValidationWebhookConfigs {
+
+  private final List<ValidationWebhookConfigApplier> configAppliers;
+
+  @Inject
+  public ValidationWebhookConfigs(
+      LifecycleManager lifecycleManager,
+      ValidationWebhookConfigApplier.Factory configApplierFactory) {
+    this.configAppliers = new ArrayList<>();
+
+    for (String customResourceName : CUSTOM_RESOURCES) {
+      this.configAppliers.add(configApplierFactory.create(customResourceName, VERSIONS));
+    }
+
+    lifecycleManager.addShutdownHook(
+        new Runnable() {
+
+          @Override
+          public void run() {
+            delete();
+          }
+        });
+  }
+
+  public void apply() throws Exception {
+    for (ValidationWebhookConfigApplier applier : configAppliers) {
+      applier.apply();
+    }
+  }
+
+  public void delete() {
+    for (ValidationWebhookConfigApplier applier : configAppliers) {
+      applier.delete();
+    }
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/GerritClusterReconciler.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/GerritClusterReconciler.java
new file mode 100644
index 0000000..c00d51e
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/GerritClusterReconciler.java
@@ -0,0 +1,149 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.cluster;
+
+import static com.google.gerrit.k8s.operator.cluster.GerritClusterReconciler.CLUSTER_MANAGED_GERRIT_EVENT_SOURCE;
+import static com.google.gerrit.k8s.operator.cluster.GerritClusterReconciler.CLUSTER_MANAGED_GERRIT_NETWORK_EVENT_SOURCE;
+import static com.google.gerrit.k8s.operator.cluster.GerritClusterReconciler.CLUSTER_MANAGED_RECEIVER_EVENT_SOURCE;
+import static com.google.gerrit.k8s.operator.cluster.GerritClusterReconciler.CM_EVENT_SOURCE;
+import static com.google.gerrit.k8s.operator.cluster.GerritClusterReconciler.PVC_EVENT_SOURCE;
+
+import com.google.gerrit.k8s.operator.cluster.dependent.ClusterManagedGerrit;
+import com.google.gerrit.k8s.operator.cluster.dependent.ClusterManagedGerritCondition;
+import com.google.gerrit.k8s.operator.cluster.dependent.ClusterManagedGerritNetwork;
+import com.google.gerrit.k8s.operator.cluster.dependent.ClusterManagedGerritNetworkCondition;
+import com.google.gerrit.k8s.operator.cluster.dependent.ClusterManagedReceiver;
+import com.google.gerrit.k8s.operator.cluster.dependent.ClusterManagedReceiverCondition;
+import com.google.gerrit.k8s.operator.cluster.dependent.NfsIdmapdConfigMap;
+import com.google.gerrit.k8s.operator.cluster.dependent.NfsWorkaroundCondition;
+import com.google.gerrit.k8s.operator.cluster.dependent.SharedPVC;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritClusterStatus;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplate;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.Receiver;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.ReceiverTemplate;
+import com.google.inject.Singleton;
+import io.fabric8.kubernetes.api.model.ConfigMap;
+import io.fabric8.kubernetes.api.model.PersistentVolumeClaim;
+import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
+import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer;
+import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
+import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
+import io.javaoperatorsdk.operator.processing.event.source.EventSource;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Singleton
+@ControllerConfiguration(
+    dependents = {
+      @Dependent(
+          name = "shared-pvc",
+          type = SharedPVC.class,
+          useEventSourceWithName = PVC_EVENT_SOURCE),
+      @Dependent(
+          type = NfsIdmapdConfigMap.class,
+          reconcilePrecondition = NfsWorkaroundCondition.class,
+          useEventSourceWithName = CM_EVENT_SOURCE),
+      @Dependent(
+          name = "gerrits",
+          type = ClusterManagedGerrit.class,
+          reconcilePrecondition = ClusterManagedGerritCondition.class,
+          useEventSourceWithName = CLUSTER_MANAGED_GERRIT_EVENT_SOURCE),
+      @Dependent(
+          name = "receiver",
+          type = ClusterManagedReceiver.class,
+          reconcilePrecondition = ClusterManagedReceiverCondition.class,
+          useEventSourceWithName = CLUSTER_MANAGED_RECEIVER_EVENT_SOURCE),
+      @Dependent(
+          type = ClusterManagedGerritNetwork.class,
+          reconcilePrecondition = ClusterManagedGerritNetworkCondition.class,
+          useEventSourceWithName = CLUSTER_MANAGED_GERRIT_NETWORK_EVENT_SOURCE),
+    })
+public class GerritClusterReconciler
+    implements Reconciler<GerritCluster>, EventSourceInitializer<GerritCluster> {
+  public static final String CM_EVENT_SOURCE = "cm-event-source";
+  public static final String PVC_EVENT_SOURCE = "pvc-event-source";
+  public static final String CLUSTER_MANAGED_GERRIT_EVENT_SOURCE = "cluster-managed-gerrit";
+  public static final String CLUSTER_MANAGED_RECEIVER_EVENT_SOURCE = "cluster-managed-receiver";
+  public static final String CLUSTER_MANAGED_GERRIT_NETWORK_EVENT_SOURCE =
+      "cluster-managed-gerrit-network";
+
+  @Override
+  public Map<String, EventSource> prepareEventSources(EventSourceContext<GerritCluster> context) {
+    InformerEventSource<ConfigMap, GerritCluster> cmEventSource =
+        new InformerEventSource<>(
+            InformerConfiguration.from(ConfigMap.class, context).build(), context);
+
+    InformerEventSource<PersistentVolumeClaim, GerritCluster> pvcEventSource =
+        new InformerEventSource<>(
+            InformerConfiguration.from(PersistentVolumeClaim.class, context).build(), context);
+
+    InformerEventSource<Gerrit, GerritCluster> clusterManagedGerritEventSource =
+        new InformerEventSource<>(
+            InformerConfiguration.from(Gerrit.class, context).build(), context);
+
+    InformerEventSource<Receiver, GerritCluster> clusterManagedReceiverEventSource =
+        new InformerEventSource<>(
+            InformerConfiguration.from(Receiver.class, context).build(), context);
+
+    InformerEventSource<GerritNetwork, GerritCluster> clusterManagedGerritNetworkEventSource =
+        new InformerEventSource<>(
+            InformerConfiguration.from(GerritNetwork.class, context).build(), context);
+
+    Map<String, EventSource> eventSources = new HashMap<>();
+    eventSources.put(CM_EVENT_SOURCE, cmEventSource);
+    eventSources.put(PVC_EVENT_SOURCE, pvcEventSource);
+    eventSources.put(CLUSTER_MANAGED_GERRIT_EVENT_SOURCE, clusterManagedGerritEventSource);
+    eventSources.put(CLUSTER_MANAGED_RECEIVER_EVENT_SOURCE, clusterManagedReceiverEventSource);
+    eventSources.put(
+        CLUSTER_MANAGED_GERRIT_NETWORK_EVENT_SOURCE, clusterManagedGerritNetworkEventSource);
+    return eventSources;
+  }
+
+  @Override
+  public UpdateControl<GerritCluster> reconcile(
+      GerritCluster gerritCluster, Context<GerritCluster> context) {
+    List<GerritTemplate> managedGerrits = gerritCluster.getSpec().getGerrits();
+    Map<String, List<String>> members = new HashMap<>();
+    members.put(
+        "gerrit",
+        managedGerrits.stream().map(g -> g.getMetadata().getName()).collect(Collectors.toList()));
+    ReceiverTemplate managedReceiver = gerritCluster.getSpec().getReceiver();
+    if (managedReceiver != null) {
+      members.put("receiver", List.of(managedReceiver.getMetadata().getName()));
+    }
+    return UpdateControl.patchStatus(updateStatus(gerritCluster, members));
+  }
+
+  private GerritCluster updateStatus(
+      GerritCluster gerritCluster, Map<String, List<String>> members) {
+    GerritClusterStatus status = gerritCluster.getStatus();
+    if (status == null) {
+      status = new GerritClusterStatus();
+    }
+    status.setMembers(members);
+    gerritCluster.setStatus(status);
+    return gerritCluster;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerrit.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerrit.java
new file mode 100644
index 0000000..8de0b3e
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerrit.java
@@ -0,0 +1,66 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.cluster.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplate;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected;
+import io.javaoperatorsdk.operator.processing.dependent.BulkDependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.Creator;
+import io.javaoperatorsdk.operator.processing.dependent.Updater;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+public class ClusterManagedGerrit extends KubernetesDependentResource<Gerrit, GerritCluster>
+    implements Creator<Gerrit, GerritCluster>,
+        Updater<Gerrit, GerritCluster>,
+        Deleter<GerritCluster>,
+        BulkDependentResource<Gerrit, GerritCluster>,
+        GarbageCollected<GerritCluster> {
+
+  public ClusterManagedGerrit() {
+    super(Gerrit.class);
+  }
+
+  @Override
+  public Map<String, Gerrit> desiredResources(
+      GerritCluster gerritCluster, Context<GerritCluster> context) {
+    Map<String, Gerrit> gerrits = new HashMap<>();
+    for (GerritTemplate template : gerritCluster.getSpec().getGerrits()) {
+      gerrits.put(template.getMetadata().getName(), desired(gerritCluster, template));
+    }
+    return gerrits;
+  }
+
+  private Gerrit desired(GerritCluster gerritCluster, GerritTemplate template) {
+    return template.toGerrit(gerritCluster);
+  }
+
+  @Override
+  public Map<String, Gerrit> getSecondaryResources(
+      GerritCluster primary, Context<GerritCluster> context) {
+    Set<Gerrit> gerrits = context.getSecondaryResources(Gerrit.class);
+    Map<String, Gerrit> result = new HashMap<>(gerrits.size());
+    for (Gerrit gerrit : gerrits) {
+      result.put(gerrit.getMetadata().getName(), gerrit);
+    }
+    return result;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerritCondition.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerritCondition.java
new file mode 100644
index 0000000..7455aca
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerritCondition.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.cluster.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
+
+public class ClusterManagedGerritCondition implements Condition<Gerrit, GerritCluster> {
+
+  @Override
+  public boolean isMet(
+      DependentResource<Gerrit, GerritCluster> dependentResource,
+      GerritCluster gerritCluster,
+      Context<GerritCluster> context) {
+    return !gerritCluster.getSpec().getGerrits().isEmpty();
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerritNetwork.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerritNetwork.java
new file mode 100644
index 0000000..232c791
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerritNetwork.java
@@ -0,0 +1,81 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.cluster.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplate;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplateSpec.GerritMode;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetworkSpec;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.NetworkMember;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.NetworkMemberWithSsh;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.ReceiverTemplate;
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+import java.util.Optional;
+
+@KubernetesDependent
+public class ClusterManagedGerritNetwork
+    extends CRUDKubernetesDependentResource<GerritNetwork, GerritCluster> {
+  public static final String NAME_SUFFIX = "gerrit-network";
+
+  public ClusterManagedGerritNetwork() {
+    super(GerritNetwork.class);
+  }
+
+  @Override
+  public GerritNetwork desired(GerritCluster gerritCluster, Context<GerritCluster> context) {
+    GerritNetwork gerritNetwork = new GerritNetwork();
+    gerritNetwork.setMetadata(
+        new ObjectMetaBuilder()
+            .withName(gerritCluster.getDependentResourceName(NAME_SUFFIX))
+            .withNamespace(gerritCluster.getMetadata().getNamespace())
+            .build());
+    GerritNetworkSpec gerritNetworkSpec = new GerritNetworkSpec();
+
+    Optional<GerritTemplate> optionalPrimaryGerrit =
+        gerritCluster.getSpec().getGerrits().stream()
+            .filter(g -> g.getSpec().getMode().equals(GerritMode.PRIMARY))
+            .findFirst();
+    if (optionalPrimaryGerrit.isPresent()) {
+      GerritTemplate primaryGerrit = optionalPrimaryGerrit.get();
+      gerritNetworkSpec.setPrimaryGerrit(
+          new NetworkMemberWithSsh(
+              primaryGerrit.getMetadata().getName(), primaryGerrit.getSpec().getService()));
+    }
+
+    Optional<GerritTemplate> optionalGerritReplica =
+        gerritCluster.getSpec().getGerrits().stream()
+            .filter(g -> g.getSpec().getMode().equals(GerritMode.REPLICA))
+            .findFirst();
+    if (optionalGerritReplica.isPresent()) {
+      GerritTemplate gerritReplica = optionalGerritReplica.get();
+      gerritNetworkSpec.setGerritReplica(
+          new NetworkMemberWithSsh(
+              gerritReplica.getMetadata().getName(), gerritReplica.getSpec().getService()));
+    }
+
+    ReceiverTemplate receiver = gerritCluster.getSpec().getReceiver();
+    if (receiver != null) {
+      gerritNetworkSpec.setReceiver(
+          new NetworkMember(receiver.getMetadata().getName(), receiver.getSpec().getService()));
+    }
+    gerritNetworkSpec.setIngress(gerritCluster.getSpec().getIngress());
+    gerritNetwork.setSpec(gerritNetworkSpec);
+    return gerritNetwork;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerritNetworkCondition.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerritNetworkCondition.java
new file mode 100644
index 0000000..a5b9244
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerritNetworkCondition.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.cluster.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
+
+public class ClusterManagedGerritNetworkCondition
+    implements Condition<GerritNetwork, GerritCluster> {
+
+  @Override
+  public boolean isMet(
+      DependentResource<GerritNetwork, GerritCluster> dependentResource,
+      GerritCluster gerritCluster,
+      Context<GerritCluster> context) {
+    return gerritCluster.getSpec().getIngress().isEnabled();
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedReceiver.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedReceiver.java
new file mode 100644
index 0000000..62618a2
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedReceiver.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.cluster.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.Receiver;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
+
+public class ClusterManagedReceiver
+    extends CRUDKubernetesDependentResource<Receiver, GerritCluster> {
+
+  public ClusterManagedReceiver() {
+    super(Receiver.class);
+  }
+
+  @Override
+  public Receiver desired(GerritCluster gerritCluster, Context<GerritCluster> context) {
+    return gerritCluster.getSpec().getReceiver().toReceiver(gerritCluster);
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedReceiverCondition.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedReceiverCondition.java
new file mode 100644
index 0000000..aa27446
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedReceiverCondition.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.cluster.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.Receiver;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
+
+public class ClusterManagedReceiverCondition implements Condition<Receiver, GerritCluster> {
+
+  @Override
+  public boolean isMet(
+      DependentResource<Receiver, GerritCluster> dependentResource,
+      GerritCluster gerritCluster,
+      Context<GerritCluster> context) {
+    return gerritCluster.getSpec().getReceiver() != null;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/NfsIdmapdConfigMap.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/NfsIdmapdConfigMap.java
new file mode 100644
index 0000000..623e801
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/NfsIdmapdConfigMap.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.cluster.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import io.fabric8.kubernetes.api.model.ConfigMap;
+import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+import java.util.Map;
+
+@KubernetesDependent(resourceDiscriminator = NfsIdmapdConfigMapDiscriminator.class)
+public class NfsIdmapdConfigMap extends CRUDKubernetesDependentResource<ConfigMap, GerritCluster> {
+  public static final String NFS_IDMAPD_CM_NAME = "nfs-idmapd-config";
+
+  public NfsIdmapdConfigMap() {
+    super(ConfigMap.class);
+  }
+
+  @Override
+  protected ConfigMap desired(GerritCluster gerritCluster, Context<GerritCluster> context) {
+    return new ConfigMapBuilder()
+        .withNewMetadata()
+        .withName(NFS_IDMAPD_CM_NAME)
+        .withNamespace(gerritCluster.getMetadata().getNamespace())
+        .withLabels(gerritCluster.getLabels(NFS_IDMAPD_CM_NAME, this.getClass().getSimpleName()))
+        .endMetadata()
+        .withData(
+            Map.of(
+                "idmapd.conf",
+                gerritCluster
+                    .getSpec()
+                    .getStorage()
+                    .getStorageClasses()
+                    .getNfsWorkaround()
+                    .getIdmapdConfig()))
+        .build();
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/NfsIdmapdConfigMapDiscriminator.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/NfsIdmapdConfigMapDiscriminator.java
new file mode 100644
index 0000000..17dfea7
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/NfsIdmapdConfigMapDiscriminator.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.cluster.dependent;
+
+import static com.google.gerrit.k8s.operator.cluster.GerritClusterReconciler.CM_EVENT_SOURCE;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import io.fabric8.kubernetes.api.model.ConfigMap;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.Optional;
+
+public class NfsIdmapdConfigMapDiscriminator
+    implements ResourceDiscriminator<ConfigMap, GerritCluster> {
+  @Override
+  public Optional<ConfigMap> distinguish(
+      Class<ConfigMap> resource, GerritCluster primary, Context<GerritCluster> context) {
+    InformerEventSource<ConfigMap, GerritCluster> ies =
+        (InformerEventSource<ConfigMap, GerritCluster>)
+            context
+                .eventSourceRetriever()
+                .getResourceEventSourceFor(ConfigMap.class, CM_EVENT_SOURCE);
+
+    return ies.get(
+        new ResourceID(
+            NfsIdmapdConfigMap.NFS_IDMAPD_CM_NAME, primary.getMetadata().getNamespace()));
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/NfsWorkaroundCondition.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/NfsWorkaroundCondition.java
new file mode 100644
index 0000000..a0cccc0
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/NfsWorkaroundCondition.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.cluster.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.NfsWorkaroundConfig;
+import io.fabric8.kubernetes.api.model.ConfigMap;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
+
+public class NfsWorkaroundCondition implements Condition<ConfigMap, GerritCluster> {
+  @Override
+  public boolean isMet(
+      DependentResource<ConfigMap, GerritCluster> dependentResource,
+      GerritCluster gerritCluster,
+      Context<GerritCluster> context) {
+    NfsWorkaroundConfig cfg =
+        gerritCluster.getSpec().getStorage().getStorageClasses().getNfsWorkaround();
+    return cfg.isEnabled() && cfg.getIdmapdConfig() != null;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/SharedPVC.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/SharedPVC.java
new file mode 100644
index 0000000..099afc6
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/SharedPVC.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.cluster.dependent;
+
+import com.google.gerrit.k8s.operator.util.CRUDKubernetesDependentPVCResource;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GerritStorageConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.SharedStorage;
+import io.fabric8.kubernetes.api.model.PersistentVolumeClaim;
+import io.fabric8.kubernetes.api.model.PersistentVolumeClaimBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+import java.util.Map;
+
+@KubernetesDependent(resourceDiscriminator = SharedPVCDiscriminator.class)
+public class SharedPVC extends CRUDKubernetesDependentPVCResource<GerritCluster> {
+
+  public static final String SHARED_PVC_NAME = "shared-pvc";
+
+  @Override
+  protected PersistentVolumeClaim desiredPVC(
+      GerritCluster gerritCluster, Context<GerritCluster> context) {
+    GerritStorageConfig storageConfig = gerritCluster.getSpec().getStorage();
+    SharedStorage sharedStorage = storageConfig.getSharedStorage();
+    return new PersistentVolumeClaimBuilder()
+        .withNewMetadata()
+        .withName(SHARED_PVC_NAME)
+        .withNamespace(gerritCluster.getMetadata().getNamespace())
+        .withLabels(gerritCluster.getLabels("shared-storage", this.getClass().getSimpleName()))
+        .endMetadata()
+        .withNewSpec()
+        .withAccessModes("ReadWriteMany")
+        .withNewResources()
+        .withRequests(Map.of("storage", sharedStorage.getSize()))
+        .endResources()
+        .withStorageClassName(storageConfig.getStorageClasses().getReadWriteMany())
+        .withSelector(sharedStorage.getSelector())
+        .withVolumeName(sharedStorage.getVolumeName())
+        .endSpec()
+        .build();
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/SharedPVCDiscriminator.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/SharedPVCDiscriminator.java
new file mode 100644
index 0000000..52fe941
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/SharedPVCDiscriminator.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.cluster.dependent;
+
+import static com.google.gerrit.k8s.operator.cluster.GerritClusterReconciler.PVC_EVENT_SOURCE;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import io.fabric8.kubernetes.api.model.PersistentVolumeClaim;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.Optional;
+
+public class SharedPVCDiscriminator
+    implements ResourceDiscriminator<PersistentVolumeClaim, GerritCluster> {
+  @Override
+  public Optional<PersistentVolumeClaim> distinguish(
+      Class<PersistentVolumeClaim> resource,
+      GerritCluster primary,
+      Context<GerritCluster> context) {
+    InformerEventSource<PersistentVolumeClaim, GerritCluster> ies =
+        (InformerEventSource<PersistentVolumeClaim, GerritCluster>)
+            context
+                .eventSourceRetriever()
+                .getResourceEventSourceFor(PersistentVolumeClaim.class, PVC_EVENT_SOURCE);
+
+    return ies.get(new ResourceID(SharedPVC.SHARED_PVC_NAME, primary.getMetadata().getNamespace()));
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/GerritReconciler.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/GerritReconciler.java
new file mode 100644
index 0000000..04d0a2f
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/GerritReconciler.java
@@ -0,0 +1,146 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gerrit;
+
+import static com.google.gerrit.k8s.operator.gerrit.GerritReconciler.CONFIG_MAP_EVENT_SOURCE;
+import static com.google.gerrit.k8s.operator.gerrit.dependent.GerritSecret.CONTEXT_SECRET_VERSION_KEY;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.k8s.operator.gerrit.dependent.GerritConfigMap;
+import com.google.gerrit.k8s.operator.gerrit.dependent.GerritInitConfigMap;
+import com.google.gerrit.k8s.operator.gerrit.dependent.GerritSecret;
+import com.google.gerrit.k8s.operator.gerrit.dependent.GerritService;
+import com.google.gerrit.k8s.operator.gerrit.dependent.GerritStatefulSet;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritStatus;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import io.fabric8.kubernetes.api.model.ConfigMap;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
+import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer;
+import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
+import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
+import io.javaoperatorsdk.operator.processing.dependent.workflow.WorkflowReconcileResult;
+import io.javaoperatorsdk.operator.processing.event.source.EventSource;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+@Singleton
+@ControllerConfiguration(
+    dependents = {
+      @Dependent(name = "gerrit-secret", type = GerritSecret.class),
+      @Dependent(
+          name = "gerrit-configmap",
+          type = GerritConfigMap.class,
+          useEventSourceWithName = CONFIG_MAP_EVENT_SOURCE),
+      @Dependent(
+          name = "gerrit-init-configmap",
+          type = GerritInitConfigMap.class,
+          useEventSourceWithName = CONFIG_MAP_EVENT_SOURCE),
+      @Dependent(
+          name = "gerrit-statefulset",
+          type = GerritStatefulSet.class,
+          dependsOn = {"gerrit-configmap", "gerrit-init-configmap"}),
+      @Dependent(
+          name = "gerrit-service",
+          type = GerritService.class,
+          dependsOn = {"gerrit-statefulset"})
+    })
+public class GerritReconciler implements Reconciler<Gerrit>, EventSourceInitializer<Gerrit> {
+  public static final String CONFIG_MAP_EVENT_SOURCE = "configmap-event-source";
+
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private final KubernetesClient client;
+
+  @Inject
+  public GerritReconciler(KubernetesClient client) {
+    this.client = client;
+  }
+
+  @Override
+  public Map<String, EventSource> prepareEventSources(EventSourceContext<Gerrit> context) {
+    InformerEventSource<ConfigMap, Gerrit> configmapEventSource =
+        new InformerEventSource<>(
+            InformerConfiguration.from(ConfigMap.class, context).build(), context);
+
+    Map<String, EventSource> eventSources = new HashMap<>();
+    eventSources.put(CONFIG_MAP_EVENT_SOURCE, configmapEventSource);
+    return eventSources;
+  }
+
+  @Override
+  public UpdateControl<Gerrit> reconcile(Gerrit gerrit, Context<Gerrit> context) throws Exception {
+    return UpdateControl.patchStatus(updateStatus(gerrit, context));
+  }
+
+  private Gerrit updateStatus(Gerrit gerrit, Context<Gerrit> context) {
+    GerritStatus status = gerrit.getStatus();
+    if (status == null) {
+      status = new GerritStatus();
+    }
+    Optional<WorkflowReconcileResult> result =
+        context.managedDependentResourceContext().getWorkflowReconcileResult();
+    if (result.isPresent()) {
+      status.setReady(result.get().allDependentResourcesReady());
+    } else {
+      status.setReady(false);
+    }
+
+    Map<String, String> cmVersions = new HashMap<>();
+
+    cmVersions.put(
+        GerritConfigMap.getName(gerrit),
+        client
+            .configMaps()
+            .inNamespace(gerrit.getMetadata().getNamespace())
+            .withName(GerritConfigMap.getName(gerrit))
+            .get()
+            .getMetadata()
+            .getResourceVersion());
+
+    cmVersions.put(
+        GerritInitConfigMap.getName(gerrit),
+        client
+            .configMaps()
+            .inNamespace(gerrit.getMetadata().getNamespace())
+            .withName(GerritInitConfigMap.getName(gerrit))
+            .get()
+            .getMetadata()
+            .getResourceVersion());
+
+    logger.atFine().log("Adding ConfigMap versions: %s", cmVersions);
+    status.setAppliedConfigMapVersions(cmVersions);
+
+    Map<String, String> secretVersions = new HashMap<>();
+    Optional<String> gerritSecret =
+        context.managedDependentResourceContext().get(CONTEXT_SECRET_VERSION_KEY, String.class);
+    if (gerritSecret.isPresent()) {
+      secretVersions.put(gerrit.getSpec().getSecretRef(), gerritSecret.get());
+    }
+    logger.atFine().log("Adding Secret versions: %s", secretVersions);
+    status.setAppliedSecretVersions(secretVersions);
+
+    gerrit.setStatus(status);
+    return gerrit;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/ConfigBuilder.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/ConfigBuilder.java
new file mode 100644
index 0000000..ca58e94
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/ConfigBuilder.java
@@ -0,0 +1,91 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gerrit.config;
+
+import com.google.common.collect.ImmutableList;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+
+public abstract class ConfigBuilder {
+
+  private final ImmutableList<RequiredOption<?>> requiredOptions;
+  private final Config config;
+
+  ConfigBuilder(Config baseConfig, ImmutableList<RequiredOption<?>> requiredOptions) {
+    this.config = baseConfig;
+    this.requiredOptions = requiredOptions;
+  }
+
+  protected ConfigBuilder(String baseConfig, ImmutableList<RequiredOption<?>> requiredOptions) {
+    this.config = parseConfig(baseConfig);
+    this.requiredOptions = requiredOptions;
+  }
+
+  public Config build() {
+    ConfigValidator configValidator = new ConfigValidator(requiredOptions);
+    try {
+      configValidator.check(config);
+    } catch (InvalidGerritConfigException e) {
+      throw new IllegalStateException(e);
+    }
+    setRequiredOptions();
+    return config;
+  }
+
+  public void validate() throws InvalidGerritConfigException {
+    new ConfigValidator(requiredOptions).check(config);
+  }
+
+  public List<RequiredOption<?>> getRequiredOptions() {
+    return this.requiredOptions;
+  }
+
+  protected Config parseConfig(String text) {
+    Config cfg = new Config();
+    try {
+      cfg.fromText(text);
+    } catch (ConfigInvalidException e) {
+      throw new IllegalStateException("Invalid configuration: " + text, e);
+    }
+    return cfg;
+  }
+
+  @SuppressWarnings("unchecked")
+  private void setRequiredOptions() {
+    for (RequiredOption<?> opt : requiredOptions) {
+      if (opt.getExpected() instanceof String) {
+        config.setString(
+            opt.getSection(), opt.getSubSection(), opt.getKey(), (String) opt.getExpected());
+      } else if (opt.getExpected() instanceof Boolean) {
+        config.setBoolean(
+            opt.getSection(), opt.getSubSection(), opt.getKey(), (Boolean) opt.getExpected());
+      } else if (opt.getExpected() instanceof Set) {
+        List<String> values =
+            new ArrayList<String>(
+                Arrays.asList(
+                    config.getStringList(opt.getSection(), opt.getSubSection(), opt.getKey())));
+        List<String> expectedSet = new ArrayList<String>();
+        expectedSet.addAll((Set<String>) opt.getExpected());
+        expectedSet.removeAll(values);
+        values.addAll(expectedSet);
+        config.setStringList(opt.getSection(), opt.getSubSection(), opt.getKey(), values);
+      }
+    }
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/ConfigValidator.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/ConfigValidator.java
new file mode 100644
index 0000000..bc952a1
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/ConfigValidator.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gerrit.config;
+
+import java.util.List;
+import java.util.Set;
+import org.eclipse.jgit.lib.Config;
+
+public class ConfigValidator {
+  private final List<RequiredOption<?>> requiredOptions;
+
+  public ConfigValidator(List<RequiredOption<?>> requiredOptions) {
+    this.requiredOptions = requiredOptions;
+  }
+
+  public void check(Config cfg) throws InvalidGerritConfigException {
+    for (RequiredOption<?> opt : requiredOptions) {
+      checkOption(cfg, opt);
+    }
+  }
+
+  private void checkOption(Config cfg, RequiredOption<?> opt) throws InvalidGerritConfigException {
+    if (!optionExists(cfg, opt)) {
+      return;
+    }
+    if (opt.getExpected() instanceof Set) {
+      return;
+    } else {
+      String value = cfg.getString(opt.getSection(), opt.getSubSection(), opt.getKey());
+      if (isExpectedValue(value, opt)) {
+        return;
+      }
+      throw new InvalidGerritConfigException(value, opt);
+    }
+  }
+
+  private boolean optionExists(Config cfg, RequiredOption<?> opt) {
+    return cfg.getNames(opt.getSection(), opt.getSubSection()).contains(opt.getKey());
+  }
+
+  private boolean isExpectedValue(String value, RequiredOption<?> opt) {
+    return value.equals(opt.getExpected().toString());
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/InvalidGerritConfigException.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/InvalidGerritConfigException.java
new file mode 100644
index 0000000..6ab14bd
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/InvalidGerritConfigException.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gerrit.config;
+
+public class InvalidGerritConfigException extends Exception {
+  private static final long serialVersionUID = 1L;
+
+  public InvalidGerritConfigException(String value, RequiredOption<?> opt) {
+    super(
+        String.format(
+            "Option %s.%s.%s set to unsupported value %s. Expected %s.",
+            opt.getSection(), opt.getSubSection(), opt.getKey(), value, opt.getExpected()));
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/RequiredOption.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/RequiredOption.java
new file mode 100644
index 0000000..9f4d22c
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/RequiredOption.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gerrit.config;
+
+public class RequiredOption<T> {
+  private final String section;
+  private final String subSection;
+  private final String key;
+  private final T expected;
+
+  public RequiredOption(String section, String subSection, String key, T expected) {
+    this.section = section;
+    this.subSection = subSection;
+    this.key = key;
+    this.expected = expected;
+  }
+
+  public RequiredOption(String section, String key, T expected) {
+    this(section, null, key, expected);
+  }
+
+  public String getSection() {
+    return section;
+  }
+
+  public String getSubSection() {
+    return subSection;
+  }
+
+  public String getKey() {
+    return key;
+  }
+
+  public T getExpected() {
+    return expected;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritConfigMap.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritConfigMap.java
new file mode 100644
index 0000000..22001b9
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritConfigMap.java
@@ -0,0 +1,91 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gerrit.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import com.google.gerrit.k8s.operator.v1alpha.gerrit.config.GerritConfigBuilder;
+import com.google.gerrit.k8s.operator.v1alpha.gerrit.config.HighAvailabilityPluginConfigBuilder;
+import com.google.gerrit.k8s.operator.v1alpha.gerrit.config.SpannerRefDbPluginConfigBuilder;
+import com.google.gerrit.k8s.operator.v1alpha.gerrit.config.ZookeeperRefDbPluginConfigBuilder;
+import io.fabric8.kubernetes.api.model.ConfigMap;
+import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+import java.util.Map;
+
+@KubernetesDependent(resourceDiscriminator = GerritConfigMapDiscriminator.class)
+public class GerritConfigMap extends CRUDKubernetesDependentResource<ConfigMap, Gerrit> {
+  private static final String DEFAULT_HEALTHCHECK_CONFIG =
+      "[healthcheck \"auth\"]\nenabled = false\n[healthcheck \"querychanges\"]\nenabled = false";
+
+  public GerritConfigMap() {
+    super(ConfigMap.class);
+  }
+
+  @Override
+  protected ConfigMap desired(Gerrit gerrit, Context<Gerrit> context) {
+    Map<String, String> gerritLabels =
+        GerritCluster.getLabels(
+            gerrit.getMetadata().getName(), getName(gerrit), this.getClass().getSimpleName());
+
+    Map<String, String> configFiles = gerrit.getSpec().getConfigFiles();
+
+    if (!configFiles.containsKey("gerrit.config")) {
+      configFiles.put("gerrit.config", "");
+    }
+
+    configFiles.put("gerrit.config", new GerritConfigBuilder(gerrit).build().toText());
+
+    if (gerrit.getSpec().isHighlyAvailablePrimary()) {
+      configFiles.put(
+          "high-availability.config",
+          new HighAvailabilityPluginConfigBuilder(gerrit).build().toText());
+    }
+
+    switch (gerrit.getSpec().getRefdb().getDatabase()) {
+      case ZOOKEEPER:
+        configFiles.put(
+            "zookeeper-refdb.config",
+            new ZookeeperRefDbPluginConfigBuilder(gerrit).build().toText());
+        break;
+      case SPANNER:
+        configFiles.put(
+            "spanner-refdb.config", new SpannerRefDbPluginConfigBuilder(gerrit).build().toText());
+        break;
+      default:
+        break;
+    }
+
+    if (!configFiles.containsKey("healthcheck.config")) {
+      configFiles.put("healthcheck.config", DEFAULT_HEALTHCHECK_CONFIG);
+    }
+
+    return new ConfigMapBuilder()
+        .withApiVersion("v1")
+        .withNewMetadata()
+        .withName(getName(gerrit))
+        .withNamespace(gerrit.getMetadata().getNamespace())
+        .withLabels(gerritLabels)
+        .endMetadata()
+        .withData(configFiles)
+        .build();
+  }
+
+  public static String getName(Gerrit gerrit) {
+    return String.format("%s-configmap", gerrit.getMetadata().getName());
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritConfigMapDiscriminator.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritConfigMapDiscriminator.java
new file mode 100644
index 0000000..6ec6b77
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritConfigMapDiscriminator.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gerrit.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import io.fabric8.kubernetes.api.model.ConfigMap;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.Optional;
+
+public class GerritConfigMapDiscriminator implements ResourceDiscriminator<ConfigMap, Gerrit> {
+  @Override
+  public Optional<ConfigMap> distinguish(
+      Class<ConfigMap> resource, Gerrit primary, Context<Gerrit> context) {
+    InformerEventSource<ConfigMap, Gerrit> ies =
+        (InformerEventSource<ConfigMap, Gerrit>)
+            context.eventSourceRetriever().getResourceEventSourceFor(ConfigMap.class);
+
+    return ies.get(
+        new ResourceID(GerritConfigMap.getName(primary), primary.getMetadata().getNamespace()));
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritInitConfigMap.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritInitConfigMap.java
new file mode 100644
index 0000000..3b9b8b4
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritInitConfigMap.java
@@ -0,0 +1,94 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gerrit.dependent;
+
+import static com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster.PLUGIN_CACHE_MOUNT_PATH;
+import static com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GlobalRefDbConfig.RefDatabase.SPANNER;
+import static com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GlobalRefDbConfig.RefDatabase.ZOOKEEPER;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritInitConfig;
+import io.fabric8.kubernetes.api.model.ConfigMap;
+import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+import java.util.Locale;
+import java.util.Map;
+
+@KubernetesDependent(resourceDiscriminator = GerritInitConfigMapDiscriminator.class)
+public class GerritInitConfigMap extends CRUDKubernetesDependentResource<ConfigMap, Gerrit> {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  public GerritInitConfigMap() {
+    super(ConfigMap.class);
+  }
+
+  @Override
+  protected ConfigMap desired(Gerrit gerrit, Context<Gerrit> context) {
+    Map<String, String> gerritLabels =
+        GerritCluster.getLabels(
+            gerrit.getMetadata().getName(), getName(gerrit), this.getClass().getSimpleName());
+
+    return new ConfigMapBuilder()
+        .withApiVersion("v1")
+        .withNewMetadata()
+        .withName(getName(gerrit))
+        .withNamespace(gerrit.getMetadata().getNamespace())
+        .withLabels(gerritLabels)
+        .endMetadata()
+        .withData(Map.of("gerrit-init.yaml", getGerritInitConfig(gerrit)))
+        .build();
+  }
+
+  private String getGerritInitConfig(Gerrit gerrit) {
+    GerritInitConfig config = new GerritInitConfig();
+    config.setPlugins(gerrit.getSpec().getPlugins());
+    config.setLibs(gerrit.getSpec().getLibs());
+    config.setPluginCacheEnabled(gerrit.getSpec().getStorage().getPluginCache().isEnabled());
+    config.setPluginCacheDir(PLUGIN_CACHE_MOUNT_PATH);
+    config.setHighlyAvailable(gerrit.getSpec().isHighlyAvailablePrimary());
+
+    switch (gerrit.getSpec().getRefdb().getDatabase()) {
+      case ZOOKEEPER:
+        config.setRefdb(ZOOKEEPER.toString().toLowerCase(Locale.US));
+        break;
+      case SPANNER:
+        config.setRefdb(SPANNER.toString().toLowerCase(Locale.US));
+        break;
+      default:
+        break;
+    }
+
+    ObjectMapper mapper =
+        new ObjectMapper(new YAMLFactory().disable(Feature.WRITE_DOC_START_MARKER));
+    try {
+      return mapper.writeValueAsString(config);
+    } catch (JsonProcessingException e) {
+      logger.atSevere().withCause(e).log("Could not serialize gerrit-init.config");
+      throw new IllegalStateException(e);
+    }
+  }
+
+  public static String getName(Gerrit gerrit) {
+    return String.format("%s-init-configmap", gerrit.getMetadata().getName());
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritInitConfigMapDiscriminator.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritInitConfigMapDiscriminator.java
new file mode 100644
index 0000000..5494f5a
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritInitConfigMapDiscriminator.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gerrit.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import io.fabric8.kubernetes.api.model.ConfigMap;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.Optional;
+
+public class GerritInitConfigMapDiscriminator implements ResourceDiscriminator<ConfigMap, Gerrit> {
+  @Override
+  public Optional<ConfigMap> distinguish(
+      Class<ConfigMap> resource, Gerrit primary, Context<Gerrit> context) {
+    InformerEventSource<ConfigMap, Gerrit> ies =
+        (InformerEventSource<ConfigMap, Gerrit>)
+            context.eventSourceRetriever().getResourceEventSourceFor(ConfigMap.class);
+
+    return ies.get(
+        new ResourceID(GerritInitConfigMap.getName(primary), primary.getMetadata().getNamespace()));
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritSecret.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritSecret.java
new file mode 100644
index 0000000..ac65dcd
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritSecret.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gerrit.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import io.fabric8.kubernetes.api.model.Secret;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@KubernetesDependent
+public class GerritSecret extends KubernetesDependentResource<Secret, Gerrit>
+    implements SecondaryToPrimaryMapper<Secret> {
+
+  public static final String CONTEXT_SECRET_VERSION_KEY = "gerrit-secret-version";
+
+  public GerritSecret() {
+    super(Secret.class);
+  }
+
+  @Override
+  public Set<ResourceID> toPrimaryResourceIDs(Secret secret) {
+    return client
+        .resources(Gerrit.class)
+        .inNamespace(secret.getMetadata().getNamespace())
+        .list()
+        .getItems()
+        .stream()
+        .filter(g -> g.getSpec().getSecretRef().equals(secret.getMetadata().getName()))
+        .map(g -> ResourceID.fromResource(g))
+        .collect(Collectors.toSet());
+  }
+
+  @Override
+  protected ReconcileResult<Secret> reconcile(
+      Gerrit primary, Secret actualResource, Context<Gerrit> context) {
+    Secret sec =
+        client
+            .secrets()
+            .inNamespace(primary.getMetadata().getNamespace())
+            .withName(primary.getSpec().getSecretRef())
+            .get();
+    if (sec != null) {
+      context
+          .managedDependentResourceContext()
+          .put(CONTEXT_SECRET_VERSION_KEY, sec.getMetadata().getResourceVersion());
+    }
+    return ReconcileResult.noOperation(actualResource);
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritService.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritService.java
new file mode 100644
index 0000000..888903c
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritService.java
@@ -0,0 +1,108 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gerrit.dependent;
+
+import static com.google.gerrit.k8s.operator.gerrit.dependent.GerritStatefulSet.HTTP_PORT;
+import static com.google.gerrit.k8s.operator.gerrit.dependent.GerritStatefulSet.SSH_PORT;
+
+import com.google.gerrit.k8s.operator.gerrit.GerritReconciler;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplate;
+import io.fabric8.kubernetes.api.model.Service;
+import io.fabric8.kubernetes.api.model.ServiceBuilder;
+import io.fabric8.kubernetes.api.model.ServicePort;
+import io.fabric8.kubernetes.api.model.ServicePortBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+@KubernetesDependent
+public class GerritService extends CRUDKubernetesDependentResource<Service, Gerrit> {
+  public static final String HTTP_PORT_NAME = "http";
+
+  public GerritService() {
+    super(Service.class);
+  }
+
+  @Override
+  protected Service desired(Gerrit gerrit, Context<Gerrit> context) {
+    return new ServiceBuilder()
+        .withApiVersion("v1")
+        .withNewMetadata()
+        .withName(getName(gerrit))
+        .withNamespace(gerrit.getMetadata().getNamespace())
+        .withLabels(getLabels(gerrit))
+        .endMetadata()
+        .withNewSpec()
+        .withType(gerrit.getSpec().getService().getType())
+        .withPorts(getServicePorts(gerrit))
+        .withSelector(GerritStatefulSet.getSelectorLabels(gerrit))
+        .endSpec()
+        .build();
+  }
+
+  public static String getName(Gerrit gerrit) {
+    return gerrit.getMetadata().getName();
+  }
+
+  public static String getName(String gerritName) {
+    return gerritName;
+  }
+
+  public static String getName(GerritTemplate gerrit) {
+    return gerrit.getMetadata().getName();
+  }
+
+  public static String getHostname(Gerrit gerrit) {
+    return getHostname(gerrit.getMetadata().getName(), gerrit.getMetadata().getNamespace());
+  }
+
+  public static String getHostname(String name, String namespace) {
+    return String.format("%s.%s.svc.cluster.local", name, namespace);
+  }
+
+  public static String getUrl(Gerrit gerrit) {
+    return String.format(
+        "http://%s:%s", getHostname(gerrit), gerrit.getSpec().getService().getHttpPort());
+  }
+
+  public static Map<String, String> getLabels(Gerrit gerrit) {
+    return GerritCluster.getLabels(
+        gerrit.getMetadata().getName(), "gerrit-service", GerritReconciler.class.getSimpleName());
+  }
+
+  private static List<ServicePort> getServicePorts(Gerrit gerrit) {
+    List<ServicePort> ports = new ArrayList<>();
+    ports.add(
+        new ServicePortBuilder()
+            .withName(HTTP_PORT_NAME)
+            .withPort(gerrit.getSpec().getService().getHttpPort())
+            .withNewTargetPort(HTTP_PORT)
+            .build());
+    if (gerrit.isSshEnabled()) {
+      ports.add(
+          new ServicePortBuilder()
+              .withName("ssh")
+              .withPort(gerrit.getSpec().getService().getSshPort())
+              .withNewTargetPort(SSH_PORT)
+              .build());
+    }
+    return ports;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritStatefulSet.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritStatefulSet.java
new file mode 100644
index 0000000..d319f4c
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritStatefulSet.java
@@ -0,0 +1,364 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gerrit.dependent;
+
+import static com.google.gerrit.k8s.operator.gerrit.dependent.GerritSecret.CONTEXT_SECRET_VERSION_KEY;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.k8s.operator.gerrit.GerritReconciler;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.ContainerImageConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.NfsWorkaroundConfig;
+import io.fabric8.kubernetes.api.model.Container;
+import io.fabric8.kubernetes.api.model.ContainerPort;
+import io.fabric8.kubernetes.api.model.EnvVar;
+import io.fabric8.kubernetes.api.model.EnvVarBuilder;
+import io.fabric8.kubernetes.api.model.Volume;
+import io.fabric8.kubernetes.api.model.VolumeBuilder;
+import io.fabric8.kubernetes.api.model.VolumeMount;
+import io.fabric8.kubernetes.api.model.VolumeMountBuilder;
+import io.fabric8.kubernetes.api.model.apps.StatefulSet;
+import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+import java.sql.Timestamp;
+import java.text.SimpleDateFormat;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+@KubernetesDependent
+public class GerritStatefulSet extends CRUDKubernetesDependentResource<StatefulSet, Gerrit> {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  private static final SimpleDateFormat RFC3339 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
+
+  private static final String SITE_VOLUME_NAME = "gerrit-site";
+  public static final int HTTP_PORT = 8080;
+  public static final int SSH_PORT = 29418;
+  public static final int JGROUPS_PORT = 7800;
+  public static final int DEBUG_PORT = 8000;
+
+  public GerritStatefulSet() {
+    super(StatefulSet.class);
+  }
+
+  @Override
+  protected StatefulSet desired(Gerrit gerrit, Context<Gerrit> context) {
+    StatefulSetBuilder stsBuilder = new StatefulSetBuilder();
+
+    List<Container> initContainers = new ArrayList<>();
+
+    NfsWorkaroundConfig nfsWorkaround =
+        gerrit.getSpec().getStorage().getStorageClasses().getNfsWorkaround();
+    if (nfsWorkaround.isEnabled() && nfsWorkaround.isChownOnStartup()) {
+      boolean hasIdmapdConfig =
+          gerrit.getSpec().getStorage().getStorageClasses().getNfsWorkaround().getIdmapdConfig()
+              != null;
+      ContainerImageConfig images = gerrit.getSpec().getContainerImages();
+
+      if (gerrit.getSpec().isHighlyAvailablePrimary()) {
+
+        initContainers.add(
+            GerritCluster.createNfsInitContainer(
+                hasIdmapdConfig, images, List.of(GerritCluster.getHAShareVolumeMount())));
+      } else {
+        initContainers.add(GerritCluster.createNfsInitContainer(hasIdmapdConfig, images));
+      }
+    }
+
+    Map<String, String> replicaSetAnnotations = new HashMap<>();
+    if (gerrit.getStatus() != null && isGerritRestartRequired(gerrit, context)) {
+      replicaSetAnnotations.put(
+          "kubectl.kubernetes.io/restartedAt", RFC3339.format(Timestamp.from(Instant.now())));
+    } else {
+      Optional<StatefulSet> existingSts = context.getSecondaryResource(StatefulSet.class);
+      if (existingSts.isPresent()) {
+        Map<String, String> existingAnnotations =
+            existingSts.get().getSpec().getTemplate().getMetadata().getAnnotations();
+        if (existingAnnotations.containsKey("kubectl.kubernetes.io/restartedAt")) {
+          replicaSetAnnotations.put(
+              "kubectl.kubernetes.io/restartedAt",
+              existingAnnotations.get("kubectl.kubernetes.io/restartedAt"));
+        }
+      }
+    }
+
+    stsBuilder
+        .withApiVersion("apps/v1")
+        .withNewMetadata()
+        .withName(gerrit.getMetadata().getName())
+        .withNamespace(gerrit.getMetadata().getNamespace())
+        .withLabels(getLabels(gerrit))
+        .endMetadata()
+        .withNewSpec()
+        .withServiceName(GerritService.getName(gerrit))
+        .withReplicas(gerrit.getSpec().getReplicas())
+        .withNewUpdateStrategy()
+        .withNewRollingUpdate()
+        .withPartition(gerrit.getSpec().getUpdatePartition())
+        .endRollingUpdate()
+        .endUpdateStrategy()
+        .withNewSelector()
+        .withMatchLabels(getSelectorLabels(gerrit))
+        .endSelector()
+        .withNewTemplate()
+        .withNewMetadata()
+        .withAnnotations(replicaSetAnnotations)
+        .withLabels(getLabels(gerrit))
+        .endMetadata()
+        .withNewSpec()
+        .withServiceAccount(gerrit.getSpec().getServiceAccount())
+        .withTolerations(gerrit.getSpec().getTolerations())
+        .withTopologySpreadConstraints(gerrit.getSpec().getTopologySpreadConstraints())
+        .withAffinity(gerrit.getSpec().getAffinity())
+        .withPriorityClassName(gerrit.getSpec().getPriorityClassName())
+        .withTerminationGracePeriodSeconds(gerrit.getSpec().getGracefulStopTimeout())
+        .addAllToImagePullSecrets(gerrit.getSpec().getContainerImages().getImagePullSecrets())
+        .withNewSecurityContext()
+        .withFsGroup(100L)
+        .endSecurityContext()
+        .addNewInitContainer()
+        .withName("gerrit-init")
+        .withEnv(getEnvVars(gerrit))
+        .withImagePullPolicy(gerrit.getSpec().getContainerImages().getImagePullPolicy())
+        .withImage(
+            gerrit.getSpec().getContainerImages().getGerritImages().getFullImageName("gerrit-init"))
+        .withResources(gerrit.getSpec().getResources())
+        .addAllToVolumeMounts(getVolumeMounts(gerrit, true))
+        .endInitContainer()
+        .addAllToInitContainers(initContainers)
+        .addNewContainer()
+        .withName("gerrit")
+        .withImagePullPolicy(gerrit.getSpec().getContainerImages().getImagePullPolicy())
+        .withImage(
+            gerrit.getSpec().getContainerImages().getGerritImages().getFullImageName("gerrit"))
+        .withNewLifecycle()
+        .withNewPreStop()
+        .withNewExec()
+        .withCommand(
+            "/bin/ash/", "-c", "kill -2 $(pidof java) && tail --pid=$(pidof java) -f /dev/null")
+        .endExec()
+        .endPreStop()
+        .endLifecycle()
+        .withEnv(getEnvVars(gerrit))
+        .withPorts(getContainerPorts(gerrit))
+        .withResources(gerrit.getSpec().getResources())
+        .withStartupProbe(gerrit.getSpec().getStartupProbe())
+        .withReadinessProbe(gerrit.getSpec().getReadinessProbe())
+        .withLivenessProbe(gerrit.getSpec().getLivenessProbe())
+        .addAllToVolumeMounts(getVolumeMounts(gerrit, false))
+        .endContainer()
+        .addAllToVolumes(getVolumes(gerrit))
+        .endSpec()
+        .endTemplate()
+        .addNewVolumeClaimTemplate()
+        .withNewMetadata()
+        .withName(SITE_VOLUME_NAME)
+        .withLabels(getSelectorLabels(gerrit))
+        .endMetadata()
+        .withNewSpec()
+        .withAccessModes("ReadWriteOnce")
+        .withNewResources()
+        .withRequests(Map.of("storage", gerrit.getSpec().getSite().getSize()))
+        .endResources()
+        .withStorageClassName(gerrit.getSpec().getStorage().getStorageClasses().getReadWriteOnce())
+        .endSpec()
+        .endVolumeClaimTemplate()
+        .endSpec();
+
+    return stsBuilder.build();
+  }
+
+  private static String getComponentName(Gerrit gerrit) {
+    return String.format("gerrit-statefulset-%s", gerrit.getMetadata().getName());
+  }
+
+  public static Map<String, String> getSelectorLabels(Gerrit gerrit) {
+    return GerritCluster.getSelectorLabels(
+        gerrit.getMetadata().getName(), getComponentName(gerrit));
+  }
+
+  private static Map<String, String> getLabels(Gerrit gerrit) {
+    return GerritCluster.getLabels(
+        gerrit.getMetadata().getName(),
+        getComponentName(gerrit),
+        GerritReconciler.class.getSimpleName());
+  }
+
+  private Set<Volume> getVolumes(Gerrit gerrit) {
+    Set<Volume> volumes = new HashSet<>();
+
+    volumes.add(
+        GerritCluster.getSharedVolume(
+            gerrit.getSpec().getStorage().getSharedStorage().getExternalPVC()));
+
+    volumes.add(
+        new VolumeBuilder()
+            .withName("gerrit-init-config")
+            .withNewConfigMap()
+            .withName(GerritInitConfigMap.getName(gerrit))
+            .endConfigMap()
+            .build());
+
+    volumes.add(
+        new VolumeBuilder()
+            .withName("gerrit-config")
+            .withNewConfigMap()
+            .withName(GerritConfigMap.getName(gerrit))
+            .endConfigMap()
+            .build());
+
+    volumes.add(
+        new VolumeBuilder()
+            .withName(gerrit.getSpec().getSecretRef())
+            .withNewSecret()
+            .withSecretName(gerrit.getSpec().getSecretRef())
+            .endSecret()
+            .build());
+
+    NfsWorkaroundConfig nfsWorkaround =
+        gerrit.getSpec().getStorage().getStorageClasses().getNfsWorkaround();
+    if (nfsWorkaround.isEnabled() && nfsWorkaround.getIdmapdConfig() != null) {
+      volumes.add(GerritCluster.getNfsImapdConfigVolume());
+    }
+
+    return volumes;
+  }
+
+  private Set<VolumeMount> getVolumeMounts(Gerrit gerrit, boolean isInitContainer) {
+    Set<VolumeMount> volumeMounts = new HashSet<>();
+    volumeMounts.add(
+        new VolumeMountBuilder().withName(SITE_VOLUME_NAME).withMountPath("/var/gerrit").build());
+    if (gerrit.getSpec().isHighlyAvailablePrimary()) {
+      volumeMounts.add(GerritCluster.getHAShareVolumeMount());
+    }
+    volumeMounts.add(GerritCluster.getGitRepositoriesVolumeMount());
+    volumeMounts.add(GerritCluster.getLogsVolumeMount());
+    volumeMounts.add(
+        new VolumeMountBuilder()
+            .withName("gerrit-config")
+            .withMountPath("/var/mnt/etc/config")
+            .build());
+
+    volumeMounts.add(
+        new VolumeMountBuilder()
+            .withName(gerrit.getSpec().getSecretRef())
+            .withMountPath("/var/mnt/etc/secret")
+            .build());
+
+    if (isInitContainer) {
+      volumeMounts.add(
+          new VolumeMountBuilder()
+              .withName("gerrit-init-config")
+              .withMountPath("/var/config")
+              .build());
+
+      if (gerrit.getSpec().getStorage().getPluginCache().isEnabled()
+          && gerrit.getSpec().getPlugins().stream().anyMatch(p -> !p.isPackagedPlugin())) {
+        volumeMounts.add(GerritCluster.getPluginCacheVolumeMount());
+      }
+    }
+
+    NfsWorkaroundConfig nfsWorkaround =
+        gerrit.getSpec().getStorage().getStorageClasses().getNfsWorkaround();
+    if (nfsWorkaround.isEnabled() && nfsWorkaround.getIdmapdConfig() != null) {
+      volumeMounts.add(GerritCluster.getNfsImapdConfigVolumeMount());
+    }
+
+    return volumeMounts;
+  }
+
+  private List<ContainerPort> getContainerPorts(Gerrit gerrit) {
+    List<ContainerPort> containerPorts = new ArrayList<>();
+    containerPorts.add(new ContainerPort(HTTP_PORT, null, null, "http", null));
+
+    if (gerrit.isSshEnabled()) {
+      containerPorts.add(new ContainerPort(SSH_PORT, null, null, "ssh", null));
+    }
+
+    if (gerrit.getSpec().isHighlyAvailablePrimary()) {
+      containerPorts.add(new ContainerPort(JGROUPS_PORT, null, null, "jgroups", null));
+    }
+
+    if (gerrit.getSpec().getDebug().isEnabled()) {
+      containerPorts.add(new ContainerPort(DEBUG_PORT, null, null, "debug", null));
+    }
+
+    return containerPorts;
+  }
+
+  private List<EnvVar> getEnvVars(Gerrit gerrit) {
+    List<EnvVar> envVars = new ArrayList<>();
+    envVars.add(GerritCluster.getPodNameEnvVar());
+    if (gerrit.getSpec().isHighlyAvailablePrimary()) {
+      envVars.add(
+          new EnvVarBuilder()
+              .withName("GERRIT_URL")
+              .withValue(
+                  String.format(
+                      "http://$(POD_NAME).%s:%s", GerritService.getHostname(gerrit), HTTP_PORT))
+              .build());
+    }
+    return envVars;
+  }
+
+  private boolean isGerritRestartRequired(Gerrit gerrit, Context<Gerrit> context) {
+    if (wasConfigMapUpdated(GerritInitConfigMap.getName(gerrit), gerrit)
+        || wasConfigMapUpdated(GerritConfigMap.getName(gerrit), gerrit)) {
+      return true;
+    }
+
+    String secretName = gerrit.getSpec().getSecretRef();
+    Optional<String> gerritSecret =
+        context.managedDependentResourceContext().get(CONTEXT_SECRET_VERSION_KEY, String.class);
+    if (gerritSecret.isPresent()) {
+      String secVersion = gerritSecret.get();
+      if (!secVersion.equals(gerrit.getStatus().getAppliedSecretVersions().get(secretName))) {
+        logger.atFine().log(
+            "Looking up Secret: %s; Installed secret resource version: %s; Resource version known to Gerrit: %s",
+            secretName, secVersion, gerrit.getStatus().getAppliedSecretVersions().get(secretName));
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private boolean wasConfigMapUpdated(String configMapName, Gerrit gerrit) {
+    String configMapVersion =
+        client
+            .configMaps()
+            .inNamespace(gerrit.getMetadata().getNamespace())
+            .withName(configMapName)
+            .get()
+            .getMetadata()
+            .getResourceVersion();
+    String knownConfigMapVersion =
+        gerrit.getStatus().getAppliedConfigMapVersions().get(configMapName);
+    if (!configMapVersion.equals(knownConfigMapVersion)) {
+      logger.atInfo().log(
+          "Looking up ConfigMap: %s; Installed configmap resource version: %s; Resource version known to Gerrit: %s",
+          configMapName, configMapVersion, knownConfigMapVersion);
+      return true;
+    }
+    return false;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollectionConflictException.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollectionConflictException.java
new file mode 100644
index 0000000..8d6bb26
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollectionConflictException.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gitgc;
+
+import java.util.Collection;
+
+public class GitGarbageCollectionConflictException extends RuntimeException {
+
+  private static final long serialVersionUID = 1L;
+
+  public GitGarbageCollectionConflictException(Collection<String> projectsIntercept) {
+    super(String.format("Found conflicting GC jobs for projects: %s", projectsIntercept));
+  }
+
+  public GitGarbageCollectionConflictException(String s) {
+    super(s);
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollectionReconciler.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollectionReconciler.java
new file mode 100644
index 0000000..8d28fea
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollectionReconciler.java
@@ -0,0 +1,152 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gitgc;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.k8s.operator.gitgc.dependent.GitGarbageCollectionCronJob;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gitgc.GitGarbageCollection;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gitgc.GitGarbageCollectionStatus;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gitgc.GitGarbageCollectionStatus.GitGcState;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler;
+import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusUpdateControl;
+import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
+import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer;
+import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
+import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.EventSource;
+import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Singleton
+@ControllerConfiguration
+public class GitGarbageCollectionReconciler
+    implements Reconciler<GitGarbageCollection>,
+        EventSourceInitializer<GitGarbageCollection>,
+        ErrorStatusHandler<GitGarbageCollection> {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  private final KubernetesClient client;
+
+  private GitGarbageCollectionCronJob dependentCronJob;
+
+  @Inject
+  public GitGarbageCollectionReconciler(KubernetesClient client) {
+    this.client = client;
+    this.dependentCronJob = new GitGarbageCollectionCronJob();
+    this.dependentCronJob.setKubernetesClient(client);
+  }
+
+  @Override
+  public Map<String, EventSource> prepareEventSources(
+      EventSourceContext<GitGarbageCollection> context) {
+    final SecondaryToPrimaryMapper<GitGarbageCollection> specificProjectGitGcMapper =
+        (GitGarbageCollection gc) ->
+            context
+                .getPrimaryCache()
+                .list(gitGc -> gitGc.getSpec().getProjects().isEmpty())
+                .map(ResourceID::fromResource)
+                .collect(Collectors.toSet());
+
+    InformerEventSource<GitGarbageCollection, GitGarbageCollection> gitGcEventSource =
+        new InformerEventSource<>(
+            InformerConfiguration.from(GitGarbageCollection.class, context)
+                .withSecondaryToPrimaryMapper(specificProjectGitGcMapper)
+                .build(),
+            context);
+
+    final SecondaryToPrimaryMapper<GerritCluster> gerritClusterMapper =
+        (GerritCluster cluster) ->
+            context
+                .getPrimaryCache()
+                .list(gitGc -> gitGc.getSpec().getCluster().equals(cluster.getMetadata().getName()))
+                .map(ResourceID::fromResource)
+                .collect(Collectors.toSet());
+
+    InformerEventSource<GerritCluster, GitGarbageCollection> gerritClusterEventSource =
+        new InformerEventSource<>(
+            InformerConfiguration.from(GerritCluster.class, context)
+                .withSecondaryToPrimaryMapper(gerritClusterMapper)
+                .build(),
+            context);
+
+    return EventSourceInitializer.nameEventSources(
+        gitGcEventSource, gerritClusterEventSource, dependentCronJob.initEventSource(context));
+  }
+
+  @Override
+  public UpdateControl<GitGarbageCollection> reconcile(
+      GitGarbageCollection gitGc, Context<GitGarbageCollection> context) {
+    if (gitGc.getSpec().getProjects().isEmpty()) {
+      gitGc = excludeProjectsHandledSeparately(gitGc);
+    }
+
+    dependentCronJob.reconcile(gitGc, context);
+    return UpdateControl.updateStatus(updateGitGcStatus(gitGc));
+  }
+
+  private GitGarbageCollection updateGitGcStatus(GitGarbageCollection gitGc) {
+    GitGarbageCollectionStatus status = gitGc.getStatus();
+    if (status == null) {
+      status = new GitGarbageCollectionStatus();
+    }
+    status.setReplicateAll(gitGc.getSpec().getProjects().isEmpty());
+    status.setState(GitGcState.ACTIVE);
+    gitGc.setStatus(status);
+    return gitGc;
+  }
+
+  private GitGarbageCollection excludeProjectsHandledSeparately(GitGarbageCollection currentGitGc) {
+    List<GitGarbageCollection> gitGcs =
+        client
+            .resources(GitGarbageCollection.class)
+            .inNamespace(currentGitGc.getMetadata().getNamespace())
+            .list()
+            .getItems();
+    gitGcs.remove(currentGitGc);
+    GitGarbageCollectionStatus currentGitGcStatus = currentGitGc.getStatus();
+    currentGitGcStatus.resetExcludedProjects();
+    for (GitGarbageCollection gc : gitGcs) {
+      currentGitGcStatus.excludeProjects(gc.getSpec().getProjects());
+    }
+    currentGitGc.setStatus(currentGitGcStatus);
+
+    return currentGitGc;
+  }
+
+  @Override
+  public ErrorStatusUpdateControl<GitGarbageCollection> updateErrorStatus(
+      GitGarbageCollection gitGc, Context<GitGarbageCollection> context, Exception e) {
+    GitGarbageCollectionStatus status = new GitGarbageCollectionStatus();
+    if (e instanceof GitGarbageCollectionConflictException) {
+      status.setState(GitGcState.CONFLICT);
+    } else {
+      logger.atSevere().withCause(e).log("Failed reconcile with message: %s", e.getMessage());
+      status.setState(GitGcState.ERROR);
+    }
+    gitGc.setStatus(status);
+
+    return ErrorStatusUpdateControl.updateStatus(gitGc);
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/dependent/GitGarbageCollectionCronJob.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/dependent/GitGarbageCollectionCronJob.java
new file mode 100644
index 0000000..254bbab
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/dependent/GitGarbageCollectionCronJob.java
@@ -0,0 +1,186 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gitgc.dependent;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gitgc.GitGarbageCollection;
+import io.fabric8.kubernetes.api.model.Container;
+import io.fabric8.kubernetes.api.model.ContainerBuilder;
+import io.fabric8.kubernetes.api.model.Volume;
+import io.fabric8.kubernetes.api.model.VolumeMount;
+import io.fabric8.kubernetes.api.model.batch.v1.CronJob;
+import io.fabric8.kubernetes.api.model.batch.v1.CronJobBuilder;
+import io.fabric8.kubernetes.api.model.batch.v1.JobTemplateSpec;
+import io.fabric8.kubernetes.api.model.batch.v1.JobTemplateSpecBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+public class GitGarbageCollectionCronJob
+    extends CRUDKubernetesDependentResource<CronJob, GitGarbageCollection> {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  public GitGarbageCollectionCronJob() {
+    super(CronJob.class);
+  }
+
+  @Override
+  protected CronJob desired(GitGarbageCollection gitGc, Context<GitGarbageCollection> context) {
+    String ns = gitGc.getMetadata().getNamespace();
+    String name = gitGc.getMetadata().getName();
+    GerritCluster gerritCluster =
+        client
+            .resources(GerritCluster.class)
+            .inNamespace(ns)
+            .withName(gitGc.getSpec().getCluster())
+            .get();
+    logger.atInfo().log("Reconciling GitGc with name: %s/%s", ns, name);
+
+    Map<String, String> gitGcLabels =
+        gerritCluster.getLabels("GitGc", this.getClass().getSimpleName());
+
+    List<Container> initContainers = new ArrayList<>();
+    if (gerritCluster.getSpec().getStorage().getStorageClasses().getNfsWorkaround().isEnabled()
+        && gerritCluster
+            .getSpec()
+            .getStorage()
+            .getStorageClasses()
+            .getNfsWorkaround()
+            .isChownOnStartup()) {
+      initContainers.add(gerritCluster.createNfsInitContainer());
+    }
+
+    JobTemplateSpec gitGcJobTemplate =
+        new JobTemplateSpecBuilder()
+            .withNewSpec()
+            .withNewTemplate()
+            .withNewMetadata()
+            .withAnnotations(
+                Map.of(
+                    "sidecar.istio.io/inject",
+                    "false",
+                    "cluster-autoscaler.kubernetes.io/safe-to-evict",
+                    "false"))
+            .withLabels(gitGcLabels)
+            .endMetadata()
+            .withNewSpec()
+            .withTolerations(gitGc.getSpec().getTolerations())
+            .withAffinity(gitGc.getSpec().getAffinity())
+            .addAllToImagePullSecrets(
+                gerritCluster.getSpec().getContainerImages().getImagePullSecrets())
+            .withRestartPolicy("OnFailure")
+            .withNewSecurityContext()
+            .withFsGroup(100L)
+            .endSecurityContext()
+            .addToContainers(buildGitGcContainer(gitGc, gerritCluster))
+            .withVolumes(getVolumes(gerritCluster))
+            .endSpec()
+            .endTemplate()
+            .endSpec()
+            .build();
+
+    return new CronJobBuilder()
+        .withApiVersion("batch/v1")
+        .withNewMetadata()
+        .withNamespace(ns)
+        .withName(name)
+        .withLabels(gitGcLabels)
+        .withAnnotations(
+            Collections.singletonMap("app.kubernetes.io/managed-by", "gerrit-operator"))
+        .addNewOwnerReference()
+        .withApiVersion(gitGc.getApiVersion())
+        .withKind(gitGc.getKind())
+        .withName(name)
+        .withUid(gitGc.getMetadata().getUid())
+        .endOwnerReference()
+        .endMetadata()
+        .withNewSpec()
+        .withSchedule(gitGc.getSpec().getSchedule())
+        .withConcurrencyPolicy("Forbid")
+        .withJobTemplate(gitGcJobTemplate)
+        .endSpec()
+        .build();
+  }
+
+  private Container buildGitGcContainer(GitGarbageCollection gitGc, GerritCluster gerritCluster) {
+    List<VolumeMount> volumeMounts =
+        List.of(
+            GerritCluster.getGitRepositoriesVolumeMount("/var/gerrit/git"),
+            GerritCluster.getLogsVolumeMount("/var/log/git"));
+
+    if (gerritCluster.getSpec().getStorage().getStorageClasses().getNfsWorkaround().isEnabled()
+        && gerritCluster
+                .getSpec()
+                .getStorage()
+                .getStorageClasses()
+                .getNfsWorkaround()
+                .getIdmapdConfig()
+            != null) {
+      volumeMounts.add(GerritCluster.getNfsImapdConfigVolumeMount());
+    }
+
+    ContainerBuilder gitGcContainerBuilder =
+        new ContainerBuilder()
+            .withName("git-gc")
+            .withImagePullPolicy(gerritCluster.getSpec().getContainerImages().getImagePullPolicy())
+            .withImage(
+                gerritCluster
+                    .getSpec()
+                    .getContainerImages()
+                    .getGerritImages()
+                    .getFullImageName("git-gc"))
+            .withResources(gitGc.getSpec().getResources())
+            .withEnv(GerritCluster.getPodNameEnvVar())
+            .withVolumeMounts(volumeMounts);
+
+    ArrayList<String> args = new ArrayList<>();
+    for (String project : gitGc.getSpec().getProjects()) {
+      args.add("-p");
+      args.add(project);
+    }
+    for (String project : gitGc.getStatus().getExcludedProjects()) {
+      args.add("-s");
+      args.add(project);
+    }
+    gitGcContainerBuilder.addAllToArgs(args);
+
+    return gitGcContainerBuilder.build();
+  }
+
+  private List<Volume> getVolumes(GerritCluster gerritCluster) {
+    List<Volume> volumes = new ArrayList<>();
+
+    volumes.add(
+        GerritCluster.getSharedVolume(
+            gerritCluster.getSpec().getStorage().getSharedStorage().getExternalPVC()));
+
+    if (gerritCluster.getSpec().getStorage().getStorageClasses().getNfsWorkaround().isEnabled()) {
+      if (gerritCluster
+              .getSpec()
+              .getStorage()
+              .getStorageClasses()
+              .getNfsWorkaround()
+              .getIdmapdConfig()
+          != null) {
+        volumes.add(GerritCluster.getNfsImapdConfigVolume());
+      }
+    }
+    return volumes;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/Constants.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/Constants.java
new file mode 100644
index 0000000..1a7f3b2
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/Constants.java
@@ -0,0 +1,22 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.package com.google.gerrit.k8s.operator.network;
+
+package com.google.gerrit.k8s.operator.network;
+
+public class Constants {
+  public static String UPLOAD_PACK_URL_PATTERN = "/.*/git-upload-pack";
+  public static String INFO_REFS_PATTERN = "/.*/info/refs";
+  public static String RECEIVE_PACK_URL_PATTERN = "/.*/git-receive-pack";
+  public static String PROJECTS_URL_PATTERN = "/a/projects/.*";
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/GerritClusterIngressCondition.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/GerritClusterIngressCondition.java
new file mode 100644
index 0000000..9077f1b
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/GerritClusterIngressCondition.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.fabric8.kubernetes.api.model.networking.v1.Ingress;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
+
+public class GerritClusterIngressCondition implements Condition<Ingress, GerritNetwork> {
+
+  @Override
+  public boolean isMet(
+      DependentResource<Ingress, GerritNetwork> dependentResource,
+      GerritNetwork gerritNetwork,
+      Context<GerritNetwork> context) {
+    return gerritNetwork.getSpec().getIngress().isEnabled()
+        && (gerritNetwork.hasReceiver() || gerritNetwork.hasGerrits());
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/GerritNetworkReconcilerProvider.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/GerritNetworkReconcilerProvider.java
new file mode 100644
index 0000000..a614e92
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/GerritNetworkReconcilerProvider.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network;
+
+import com.google.gerrit.k8s.operator.network.ambassador.GerritAmbassadorReconciler;
+import com.google.gerrit.k8s.operator.network.ingress.GerritIngressReconciler;
+import com.google.gerrit.k8s.operator.network.istio.GerritIstioReconciler;
+import com.google.gerrit.k8s.operator.network.none.GerritNoIngressReconciler;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.name.Named;
+import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
+
+public class GerritNetworkReconcilerProvider implements Provider<Reconciler<GerritNetwork>> {
+  private final IngressType ingressType;
+
+  @Inject
+  public GerritNetworkReconcilerProvider(@Named("IngressType") IngressType ingressType) {
+    this.ingressType = ingressType;
+  }
+
+  @Override
+  public Reconciler<GerritNetwork> get() {
+    switch (ingressType) {
+      case INGRESS:
+        return new GerritIngressReconciler();
+      case ISTIO:
+        return new GerritIstioReconciler();
+      case AMBASSADOR:
+        return new GerritAmbassadorReconciler();
+      default:
+        return new GerritNoIngressReconciler();
+    }
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/IngressType.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/IngressType.java
new file mode 100644
index 0000000..9c5383c
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/IngressType.java
@@ -0,0 +1,22 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network;
+
+public enum IngressType {
+  NONE,
+  INGRESS,
+  ISTIO,
+  AMBASSADOR
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/GerritAmbassadorReconciler.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/GerritAmbassadorReconciler.java
new file mode 100644
index 0000000..34dc9db
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/GerritAmbassadorReconciler.java
@@ -0,0 +1,169 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador;
+
+import static com.google.gerrit.k8s.operator.network.ambassador.GerritAmbassadorReconciler.MAPPING_EVENT_SOURCE;
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterHost.GERRIT_HOST;
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMapping.GERRIT_MAPPING;
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingGETReplica.GERRIT_MAPPING_GET_REPLICA;
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingPOSTReplica.GERRIT_MAPPING_POST_REPLICA;
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingPrimary.GERRIT_MAPPING_PRIMARY;
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingReceiver.GERRIT_MAPPING_RECEIVER;
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingReceiverGET.GERRIT_MAPPING_RECEIVER_GET;
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterTLSContext.GERRIT_TLS_CONTEXT;
+
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.CreateHostCondition;
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterHost;
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMapping;
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingGETReplica;
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingPOSTReplica;
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingPrimary;
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingReceiver;
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingReceiverGET;
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterTLSContext;
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.LoadBalanceCondition;
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.ReceiverMappingCondition;
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.SingleMappingCondition;
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.TLSContextCondition;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import com.google.inject.Singleton;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
+import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer;
+import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
+import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
+import io.javaoperatorsdk.operator.processing.event.source.EventSource;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Provides an Ambassador-based implementation for GerritNetworkReconciler.
+ *
+ * <p>Creates and manages Ambassador Custom Resources using the "managed dependent resources"
+ * approach in josdk. Since multiple dependent resources of the same type (`Mapping`) need to be
+ * created, "resource discriminators" are used for each of the different Mapping dependent
+ * resources.
+ *
+ * <p>Ambassador custom resource POJOs are generated via the `java-generator-maven-plugin` in the
+ * fabric8 project.
+ *
+ * <p>Mapping logic
+ *
+ * <p>The Mappings are created based on the composition of Gerrit instances in the GerritCluster.
+ *
+ * <p>There are three cases:
+ *
+ * <p>1. 0 Primary 1 Replica
+ *
+ * <p>Direct all traffic (read/write) to the Replica
+ *
+ * <p>2. 1 Primary 0 Replica
+ *
+ * <p>Direct all traffic (read/write) to the Primary
+ *
+ * <p>3. 1 Primary 1 Replica
+ *
+ * <p>Direct write traffic to Primary and read traffic to Replica. To capture this requirement,
+ * three different Mappings have to be created.
+ *
+ * <p>Note: git fetch/clone operations result in two HTTP requests to the git server. The first is
+ * of the form `GET /my-test-repo/info/refs?service=git-upload-pack` and the second is of the form
+ * `POST /my-test-repo/git-upload-pack`.
+ *
+ * <p>Note: git push operations result in two HTTP requests to the git server. The first is of the
+ * form `GET /my-test-repo/info/refs?service=git-receive-pack` and the second is of the form `POST
+ * /my-test-repo/git-receive-pack`.
+ *
+ * <p>If a Receiver is part of the GerritCluster, additional mappings are created such that all
+ * requests that the replication plugin sends to the `adminUrl` [1] are routed to the Receiver. This
+ * includes `git push` related `GET` and `POST` requests, and requests to the `/projects` REST API
+ * endpoints.
+ *
+ * <p>[1]
+ * https://gerrit.googlesource.com/plugins/replication/+/refs/heads/master/src/main/resources/Documentation/config.md
+ */
+@Singleton
+@ControllerConfiguration(
+    dependents = {
+      @Dependent(
+          name = GERRIT_MAPPING,
+          type = GerritClusterMapping.class,
+          // Cluster has only either Primary or Replica instance
+          reconcilePrecondition = SingleMappingCondition.class,
+          useEventSourceWithName = MAPPING_EVENT_SOURCE),
+      @Dependent(
+          name = GERRIT_MAPPING_POST_REPLICA,
+          type = GerritClusterMappingPOSTReplica.class,
+          // Cluster has both Primary and Replica instances
+          reconcilePrecondition = LoadBalanceCondition.class,
+          useEventSourceWithName = MAPPING_EVENT_SOURCE),
+      @Dependent(
+          name = GERRIT_MAPPING_GET_REPLICA,
+          type = GerritClusterMappingGETReplica.class,
+          reconcilePrecondition = LoadBalanceCondition.class,
+          useEventSourceWithName = MAPPING_EVENT_SOURCE),
+      @Dependent(
+          name = GERRIT_MAPPING_PRIMARY,
+          type = GerritClusterMappingPrimary.class,
+          reconcilePrecondition = LoadBalanceCondition.class,
+          useEventSourceWithName = MAPPING_EVENT_SOURCE),
+      @Dependent(
+          name = GERRIT_MAPPING_RECEIVER,
+          type = GerritClusterMappingReceiver.class,
+          reconcilePrecondition = ReceiverMappingCondition.class,
+          useEventSourceWithName = MAPPING_EVENT_SOURCE),
+      @Dependent(
+          name = GERRIT_MAPPING_RECEIVER_GET,
+          type = GerritClusterMappingReceiverGET.class,
+          reconcilePrecondition = ReceiverMappingCondition.class,
+          useEventSourceWithName = MAPPING_EVENT_SOURCE),
+      @Dependent(
+          name = GERRIT_TLS_CONTEXT,
+          type = GerritClusterTLSContext.class,
+          reconcilePrecondition = TLSContextCondition.class),
+      @Dependent(
+          name = GERRIT_HOST,
+          type = GerritClusterHost.class,
+          reconcilePrecondition = CreateHostCondition.class),
+    })
+public class GerritAmbassadorReconciler
+    implements Reconciler<GerritNetwork>, EventSourceInitializer<GerritNetwork> {
+
+  public static final String MAPPING_EVENT_SOURCE = "mapping-event-source";
+
+  // Because we have multiple dependent resources of the same type `Mapping`, we need to specify
+  // a named event source.
+  @Override
+  public Map<String, EventSource> prepareEventSources(EventSourceContext<GerritNetwork> context) {
+    InformerEventSource<Mapping, GerritNetwork> mappingEventSource =
+        new InformerEventSource<>(
+            InformerConfiguration.from(Mapping.class, context).build(), context);
+
+    Map<String, EventSource> eventSources = new HashMap<>();
+    eventSources.put(MAPPING_EVENT_SOURCE, mappingEventSource);
+    return eventSources;
+  }
+
+  @Override
+  public UpdateControl<GerritNetwork> reconcile(
+      GerritNetwork resource, Context<GerritNetwork> context) throws Exception {
+    return UpdateControl.noUpdate();
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/AbstractAmbassadorDependentResource.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/AbstractAmbassadorDependentResource.java
new file mode 100644
index 0000000..3cc8d4a
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/AbstractAmbassadorDependentResource.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.fabric8.kubernetes.api.model.ObjectMeta;
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+import io.getambassador.v2.MappingSpec;
+import io.getambassador.v2.MappingSpecBuilder;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
+import java.util.List;
+
+public abstract class AbstractAmbassadorDependentResource<T extends HasMetadata>
+    extends CRUDKubernetesDependentResource<T, GerritNetwork> {
+
+  public AbstractAmbassadorDependentResource(Class<T> dependentResourceClass) {
+    super(dependentResourceClass);
+  }
+
+  public ObjectMeta getCommonMetadata(GerritNetwork gerritnetwork, String name, String className) {
+    ObjectMeta metadata =
+        new ObjectMetaBuilder()
+            .withName(name)
+            .withNamespace(gerritnetwork.getMetadata().getNamespace())
+            .withLabels(
+                GerritCluster.getLabels(gerritnetwork.getMetadata().getName(), name, className))
+            .build();
+    return metadata;
+  }
+
+  public MappingSpec getCommonSpec(GerritNetwork gerritnetwork, String serviceName) {
+    MappingSpec spec =
+        new MappingSpecBuilder()
+            .withAmbassadorId(getAmbassadorIds(gerritnetwork))
+            .withHost(gerritnetwork.getSpec().getIngress().getHost())
+            .withPrefix("/")
+            .withService(serviceName)
+            .withBypassAuth(true)
+            .withRewrite("") // important - so the prefix doesn't get overwritten to "/"
+            .build();
+    return spec;
+  }
+
+  public List<String> getAmbassadorIds(GerritNetwork gerritnetwork) {
+    return gerritnetwork.getSpec().getIngress().getAmbassador().getId();
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/CreateHostCondition.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/CreateHostCondition.java
new file mode 100644
index 0000000..e7f99ec
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/CreateHostCondition.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
+
+public class CreateHostCondition implements Condition<Mapping, GerritNetwork> {
+
+  @Override
+  public boolean isMet(
+      DependentResource<Mapping, GerritNetwork> dependentResource,
+      GerritNetwork gerritNetwork,
+      Context<GerritNetwork> context) {
+
+    return gerritNetwork.getSpec().getIngress().isEnabled()
+        && gerritNetwork.getSpec().getIngress().getAmbassador().getCreateHost();
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterHost.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterHost.java
new file mode 100644
index 0000000..5f916c4
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterHost.java
@@ -0,0 +1,62 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterTLSContext.GERRIT_TLS_CONTEXT;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Host;
+import io.getambassador.v2.HostBuilder;
+import io.getambassador.v2.hostspec.TlsContext;
+import io.getambassador.v2.hostspec.TlsSecret;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+
+public class GerritClusterHost extends AbstractAmbassadorDependentResource<Host> {
+
+  public static final String GERRIT_HOST = "gerrit-ambassador-host";
+
+  public GerritClusterHost() {
+    super(Host.class);
+  }
+
+  @Override
+  public Host desired(GerritNetwork gerritNetwork, Context<GerritNetwork> context) {
+
+    TlsSecret tlsSecret = null;
+    TlsContext tlsContext = null;
+
+    if (gerritNetwork.getSpec().getIngress().getTls().isEnabled()) {
+      tlsSecret = new TlsSecret();
+      tlsContext = new TlsContext();
+      tlsSecret.setName(gerritNetwork.getSpec().getIngress().getTls().getSecret());
+      tlsContext.setName(GERRIT_TLS_CONTEXT);
+    }
+
+    Host host =
+        new HostBuilder()
+            .withNewMetadataLike(
+                getCommonMetadata(gerritNetwork, GERRIT_HOST, this.getClass().getSimpleName()))
+            .endMetadata()
+            .withNewSpec()
+            .withAmbassadorId(getAmbassadorIds(gerritNetwork))
+            .withHostname(gerritNetwork.getSpec().getIngress().getHost())
+            .withTlsSecret(tlsSecret)
+            .withTlsContext(tlsContext)
+            .endSpec()
+            .build();
+
+    return host;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMapping.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMapping.java
new file mode 100644
index 0000000..733be73
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMapping.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.NetworkMemberWithSsh;
+import io.getambassador.v2.Mapping;
+import io.getambassador.v2.MappingBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+
+@KubernetesDependent(resourceDiscriminator = GerritClusterMappingDiscriminator.class)
+public class GerritClusterMapping extends AbstractAmbassadorDependentResource<Mapping>
+    implements MappingDependentResourceInterface {
+
+  public static final String GERRIT_MAPPING = "gerrit-mapping";
+
+  public GerritClusterMapping() {
+    super(Mapping.class);
+  }
+
+  @Override
+  public Mapping desired(GerritNetwork gerritNetwork, Context<GerritNetwork> context) {
+
+    // If only one Gerrit instance in GerritCluster, send all git-over-https requests to it
+    NetworkMemberWithSsh gerrit =
+        gerritNetwork.hasGerritReplica()
+            ? gerritNetwork.getSpec().getGerritReplica()
+            : gerritNetwork.getSpec().getPrimaryGerrit();
+    String serviceName = gerrit.getName() + ":" + gerrit.getHttpPort();
+    Mapping mapping =
+        new MappingBuilder()
+            .withNewMetadataLike(
+                getCommonMetadata(gerritNetwork, GERRIT_MAPPING, this.getClass().getSimpleName()))
+            .endMetadata()
+            .withNewSpecLike(getCommonSpec(gerritNetwork, serviceName))
+            .endSpec()
+            .build();
+    return mapping;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingDiscriminator.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingDiscriminator.java
new file mode 100644
index 0000000..12d99c3
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingDiscriminator.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMapping.GERRIT_MAPPING;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.Optional;
+
+public class GerritClusterMappingDiscriminator
+    implements ResourceDiscriminator<Mapping, GerritNetwork> {
+  @Override
+  public Optional<Mapping> distinguish(
+      Class<Mapping> resource, GerritNetwork network, Context<GerritNetwork> context) {
+    InformerEventSource<Mapping, GerritNetwork> ies =
+        (InformerEventSource<Mapping, GerritNetwork>)
+            context.eventSourceRetriever().getResourceEventSourceFor(Mapping.class);
+    return ies.get(new ResourceID(GERRIT_MAPPING, network.getMetadata().getNamespace()));
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingGETReplica.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingGETReplica.java
new file mode 100644
index 0000000..8fda99e
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingGETReplica.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.gerrit.k8s.operator.network.Constants.INFO_REFS_PATTERN;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.getambassador.v2.MappingBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+import java.util.HashMap;
+
+@KubernetesDependent(resourceDiscriminator = GerritClusterMappingGETReplicaDiscriminator.class)
+public class GerritClusterMappingGETReplica extends AbstractAmbassadorDependentResource<Mapping>
+    implements MappingDependentResourceInterface {
+
+  public static final String GERRIT_MAPPING_GET_REPLICA = "gerrit-mapping-get-replica";
+
+  public GerritClusterMappingGETReplica() {
+    super(Mapping.class);
+  }
+
+  @Override
+  public Mapping desired(GerritNetwork gerritNetwork, Context<GerritNetwork> context) {
+
+    String replicaServiceName =
+        gerritNetwork.getSpec().getGerritReplica().getName()
+            + ":"
+            + gerritNetwork.getSpec().getGerritReplica().getHttpPort();
+
+    // Send fetch/clone GET requests to the Replica
+    Mapping mapping =
+        new MappingBuilder()
+            .withNewMetadataLike(
+                getCommonMetadata(
+                    gerritNetwork, GERRIT_MAPPING_GET_REPLICA, this.getClass().getSimpleName()))
+            .endMetadata()
+            .withNewSpecLike(getCommonSpec(gerritNetwork, replicaServiceName))
+            .withNewV2QueryParameters()
+            .withAdditionalProperties(
+                new HashMap<String, Object>() {
+                  {
+                    put("service", "git-upload-pack");
+                  }
+                })
+            .endV2QueryParameters()
+            .withMethod("GET")
+            .withPrefix(INFO_REFS_PATTERN)
+            .withPrefixRegex(true)
+            .endSpec()
+            .build();
+
+    return mapping;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingGETReplicaDiscriminator.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingGETReplicaDiscriminator.java
new file mode 100644
index 0000000..9a4da43
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingGETReplicaDiscriminator.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingGETReplica.GERRIT_MAPPING_GET_REPLICA;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.Optional;
+
+public class GerritClusterMappingGETReplicaDiscriminator
+    implements ResourceDiscriminator<Mapping, GerritNetwork> {
+  @Override
+  public Optional<Mapping> distinguish(
+      Class<Mapping> resource, GerritNetwork network, Context<GerritNetwork> context) {
+    InformerEventSource<Mapping, GerritNetwork> ies =
+        (InformerEventSource<Mapping, GerritNetwork>)
+            context.eventSourceRetriever().getResourceEventSourceFor(Mapping.class);
+    return ies.get(
+        new ResourceID(GERRIT_MAPPING_GET_REPLICA, network.getMetadata().getNamespace()));
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPOSTReplica.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPOSTReplica.java
new file mode 100644
index 0000000..1779990
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPOSTReplica.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.gerrit.k8s.operator.network.Constants.UPLOAD_PACK_URL_PATTERN;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.getambassador.v2.MappingBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+
+@KubernetesDependent(resourceDiscriminator = GerritClusterMappingPOSTReplicaDiscriminator.class)
+public class GerritClusterMappingPOSTReplica extends AbstractAmbassadorDependentResource<Mapping>
+    implements MappingDependentResourceInterface {
+
+  public static final String GERRIT_MAPPING_POST_REPLICA = "gerrit-mapping-post-replica";
+
+  public GerritClusterMappingPOSTReplica() {
+    super(Mapping.class);
+  }
+
+  @Override
+  public Mapping desired(GerritNetwork gerritNetwork, Context<GerritNetwork> context) {
+
+    String replicaServiceName =
+        gerritNetwork.getSpec().getGerritReplica().getName()
+            + ":"
+            + gerritNetwork.getSpec().getGerritReplica().getHttpPort();
+
+    // Send fetch/clone POST requests to the Replica
+    Mapping mapping =
+        new MappingBuilder()
+            .withNewMetadataLike(
+                getCommonMetadata(
+                    gerritNetwork, GERRIT_MAPPING_POST_REPLICA, this.getClass().getSimpleName()))
+            .endMetadata()
+            .withNewSpecLike(getCommonSpec(gerritNetwork, replicaServiceName))
+            .withPrefix(UPLOAD_PACK_URL_PATTERN)
+            .withPrefixRegex(true)
+            .withMethod("POST")
+            .endSpec()
+            .build();
+    return mapping;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPOSTReplicaDiscriminator.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPOSTReplicaDiscriminator.java
new file mode 100644
index 0000000..a1c02c8
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPOSTReplicaDiscriminator.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingPOSTReplica.GERRIT_MAPPING_POST_REPLICA;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.Optional;
+
+public class GerritClusterMappingPOSTReplicaDiscriminator
+    implements ResourceDiscriminator<Mapping, GerritNetwork> {
+  @Override
+  public Optional<Mapping> distinguish(
+      Class<Mapping> resource, GerritNetwork network, Context<GerritNetwork> context) {
+    InformerEventSource<Mapping, GerritNetwork> ies =
+        (InformerEventSource<Mapping, GerritNetwork>)
+            context.eventSourceRetriever().getResourceEventSourceFor(Mapping.class);
+    return ies.get(
+        new ResourceID(GERRIT_MAPPING_POST_REPLICA, network.getMetadata().getNamespace()));
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPrimary.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPrimary.java
new file mode 100644
index 0000000..f62b74a
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPrimary.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.getambassador.v2.MappingBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+
+@KubernetesDependent(resourceDiscriminator = GerritClusterMappingPrimaryDiscriminator.class)
+public class GerritClusterMappingPrimary extends AbstractAmbassadorDependentResource<Mapping>
+    implements MappingDependentResourceInterface {
+
+  public static final String GERRIT_MAPPING_PRIMARY = "gerrit-mapping-primary";
+
+  public GerritClusterMappingPrimary() {
+    super(Mapping.class);
+  }
+
+  @Override
+  public Mapping desired(GerritNetwork gerritNetwork, Context<GerritNetwork> context) {
+
+    String primaryServiceName =
+        gerritNetwork.getSpec().getPrimaryGerrit().getName()
+            + ":"
+            + gerritNetwork.getSpec().getPrimaryGerrit().getHttpPort();
+
+    // Send all write traffic (non git fetch/clone traffic) to the Primary.
+    // Emissary evaluates more constrained Mappings first.
+    Mapping mapping =
+        new MappingBuilder()
+            .withNewMetadataLike(
+                getCommonMetadata(
+                    gerritNetwork, GERRIT_MAPPING_PRIMARY, this.getClass().getSimpleName()))
+            .endMetadata()
+            .withNewSpecLike(getCommonSpec(gerritNetwork, primaryServiceName))
+            .endSpec()
+            .build();
+
+    return mapping;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPrimaryDiscriminator.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPrimaryDiscriminator.java
new file mode 100644
index 0000000..0d38c69
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPrimaryDiscriminator.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingPrimary.GERRIT_MAPPING_PRIMARY;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.Optional;
+
+public class GerritClusterMappingPrimaryDiscriminator
+    implements ResourceDiscriminator<Mapping, GerritNetwork> {
+  @Override
+  public Optional<Mapping> distinguish(
+      Class<Mapping> resource, GerritNetwork network, Context<GerritNetwork> context) {
+    InformerEventSource<Mapping, GerritNetwork> ies =
+        (InformerEventSource<Mapping, GerritNetwork>)
+            context.eventSourceRetriever().getResourceEventSourceFor(Mapping.class);
+    return ies.get(new ResourceID(GERRIT_MAPPING_PRIMARY, network.getMetadata().getNamespace()));
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiver.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiver.java
new file mode 100644
index 0000000..2d50b65
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiver.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.gerrit.k8s.operator.network.Constants.PROJECTS_URL_PATTERN;
+import static com.google.gerrit.k8s.operator.network.Constants.RECEIVE_PACK_URL_PATTERN;
+
+import com.google.gerrit.k8s.operator.receiver.dependent.ReceiverService;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.getambassador.v2.MappingBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+
+@KubernetesDependent(resourceDiscriminator = GerritClusterMappingReceiverDiscriminator.class)
+public class GerritClusterMappingReceiver extends AbstractAmbassadorDependentResource<Mapping>
+    implements MappingDependentResourceInterface {
+
+  public static final String GERRIT_MAPPING_RECEIVER = "gerrit-mapping-receiver";
+
+  public GerritClusterMappingReceiver() {
+    super(Mapping.class);
+  }
+
+  @Override
+  public Mapping desired(GerritNetwork gerritNetwork, Context<GerritNetwork> context) {
+
+    String receiverServiceName =
+        ReceiverService.getName(gerritNetwork.getSpec().getReceiver().getName())
+            + ":"
+            + gerritNetwork.getSpec().getReceiver().getHttpPort();
+
+    Mapping mapping =
+        new MappingBuilder()
+            .withNewMetadataLike(
+                getCommonMetadata(
+                    gerritNetwork, GERRIT_MAPPING_RECEIVER, this.getClass().getSimpleName()))
+            .endMetadata()
+            .withNewSpecLike(getCommonSpec(gerritNetwork, receiverServiceName))
+            .withPrefix(PROJECTS_URL_PATTERN + "|" + RECEIVE_PACK_URL_PATTERN)
+            .withPrefixRegex(true)
+            .endSpec()
+            .build();
+    return mapping;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiverDiscriminator.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiverDiscriminator.java
new file mode 100644
index 0000000..a31e905
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiverDiscriminator.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingReceiver.GERRIT_MAPPING_RECEIVER;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.Optional;
+
+public class GerritClusterMappingReceiverDiscriminator
+    implements ResourceDiscriminator<Mapping, GerritNetwork> {
+  @Override
+  public Optional<Mapping> distinguish(
+      Class<Mapping> resource, GerritNetwork network, Context<GerritNetwork> context) {
+    InformerEventSource<Mapping, GerritNetwork> ies =
+        (InformerEventSource<Mapping, GerritNetwork>)
+            context.eventSourceRetriever().getResourceEventSourceFor(Mapping.class);
+    return ies.get(new ResourceID(GERRIT_MAPPING_RECEIVER, network.getMetadata().getNamespace()));
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiverGET.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiverGET.java
new file mode 100644
index 0000000..aadd9dc
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiverGET.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.gerrit.k8s.operator.network.Constants.INFO_REFS_PATTERN;
+
+import com.google.gerrit.k8s.operator.receiver.dependent.ReceiverService;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.getambassador.v2.MappingBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+import java.util.HashMap;
+
+@KubernetesDependent(resourceDiscriminator = GerritClusterMappingReceiverGETDiscriminator.class)
+public class GerritClusterMappingReceiverGET extends AbstractAmbassadorDependentResource<Mapping>
+    implements MappingDependentResourceInterface {
+
+  public static final String GERRIT_MAPPING_RECEIVER_GET = "gerrit-mapping-receiver-get";
+
+  public GerritClusterMappingReceiverGET() {
+    super(Mapping.class);
+  }
+
+  @Override
+  public Mapping desired(GerritNetwork gerritNetwork, Context<GerritNetwork> context) {
+
+    String receiverServiceName =
+        ReceiverService.getName(gerritNetwork.getSpec().getReceiver().getName())
+            + ":"
+            + gerritNetwork.getSpec().getReceiver().getHttpPort();
+
+    Mapping mapping =
+        new MappingBuilder()
+            .withNewMetadataLike(
+                getCommonMetadata(
+                    gerritNetwork, GERRIT_MAPPING_RECEIVER_GET, this.getClass().getSimpleName()))
+            .endMetadata()
+            .withNewSpecLike(getCommonSpec(gerritNetwork, receiverServiceName))
+            .withNewV2QueryParameters()
+            .withAdditionalProperties(
+                new HashMap<String, Object>() {
+                  {
+                    put("service", "git-receive-pack");
+                  }
+                })
+            .endV2QueryParameters()
+            .withMethod("GET")
+            .withPrefix(INFO_REFS_PATTERN)
+            .withPrefixRegex(true)
+            .endSpec()
+            .build();
+    return mapping;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiverGETDiscriminator.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiverGETDiscriminator.java
new file mode 100644
index 0000000..24cf7f8
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiverGETDiscriminator.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingReceiverGET.GERRIT_MAPPING_RECEIVER_GET;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.Optional;
+
+public class GerritClusterMappingReceiverGETDiscriminator
+    implements ResourceDiscriminator<Mapping, GerritNetwork> {
+  @Override
+  public Optional<Mapping> distinguish(
+      Class<Mapping> resource, GerritNetwork network, Context<GerritNetwork> context) {
+    InformerEventSource<Mapping, GerritNetwork> ies =
+        (InformerEventSource<Mapping, GerritNetwork>)
+            context.eventSourceRetriever().getResourceEventSourceFor(Mapping.class);
+    return ies.get(
+        new ResourceID(GERRIT_MAPPING_RECEIVER_GET, network.getMetadata().getNamespace()));
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterTLSContext.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterTLSContext.java
new file mode 100644
index 0000000..7cae8da
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterTLSContext.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.TLSContext;
+import io.getambassador.v2.TLSContextBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import java.util.List;
+
+public class GerritClusterTLSContext extends AbstractAmbassadorDependentResource<TLSContext> {
+
+  public static final String GERRIT_TLS_CONTEXT = "gerrit-tls-context";
+
+  public GerritClusterTLSContext() {
+    super(TLSContext.class);
+  }
+
+  @Override
+  protected TLSContext desired(GerritNetwork gerritNetwork, Context<GerritNetwork> context) {
+    TLSContext tlsContext =
+        new TLSContextBuilder()
+            .withNewMetadataLike(
+                getCommonMetadata(
+                    gerritNetwork, GERRIT_TLS_CONTEXT, this.getClass().getSimpleName()))
+            .endMetadata()
+            .withNewSpec()
+            .withAmbassadorId(getAmbassadorIds(gerritNetwork))
+            .withSecret(gerritNetwork.getSpec().getIngress().getTls().getSecret())
+            .withHosts(List.of(gerritNetwork.getSpec().getIngress().getHost()))
+            .withSecretNamespacing(true)
+            .endSpec()
+            .build();
+    return tlsContext;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/LoadBalanceCondition.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/LoadBalanceCondition.java
new file mode 100644
index 0000000..ea9066a
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/LoadBalanceCondition.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
+
+public class LoadBalanceCondition implements Condition<Mapping, GerritNetwork> {
+
+  @Override
+  public boolean isMet(
+      DependentResource<Mapping, GerritNetwork> dependentResource,
+      GerritNetwork gerritNetwork,
+      Context<GerritNetwork> context) {
+
+    return gerritNetwork.getSpec().getIngress().isEnabled()
+        && gerritNetwork.hasPrimaryGerrit()
+        && gerritNetwork.hasGerritReplica();
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/MappingDependentResourceInterface.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/MappingDependentResourceInterface.java
new file mode 100644
index 0000000..dc27428
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/MappingDependentResourceInterface.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+
+public interface MappingDependentResourceInterface {
+  public Mapping desired(GerritNetwork gerritNetwork, Context<GerritNetwork> context);
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/ReceiverMappingCondition.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/ReceiverMappingCondition.java
new file mode 100644
index 0000000..a83f984
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/ReceiverMappingCondition.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
+
+public class ReceiverMappingCondition implements Condition<Mapping, GerritNetwork> {
+
+  @Override
+  public boolean isMet(
+      DependentResource<Mapping, GerritNetwork> dependentResource,
+      GerritNetwork gerritNetwork,
+      Context<GerritNetwork> context) {
+
+    return gerritNetwork.getSpec().getIngress().isEnabled() && gerritNetwork.hasReceiver();
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/SingleMappingCondition.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/SingleMappingCondition.java
new file mode 100644
index 0000000..52a1f2c
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/SingleMappingCondition.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
+
+public class SingleMappingCondition implements Condition<Mapping, GerritNetwork> {
+
+  @Override
+  public boolean isMet(
+      DependentResource<Mapping, GerritNetwork> dependentResource,
+      GerritNetwork gerritNetwork,
+      Context<GerritNetwork> context) {
+
+    return gerritNetwork.getSpec().getIngress().isEnabled()
+        && (gerritNetwork.hasPrimaryGerrit() ^ gerritNetwork.hasGerritReplica());
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/TLSContextCondition.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/TLSContextCondition.java
new file mode 100644
index 0000000..053c559
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/TLSContextCondition.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
+
+public class TLSContextCondition implements Condition<Mapping, GerritNetwork> {
+
+  @Override
+  public boolean isMet(
+      DependentResource<Mapping, GerritNetwork> dependentResource,
+      GerritNetwork gerritNetwork,
+      Context<GerritNetwork> context) {
+
+    return gerritNetwork.getSpec().getIngress().isEnabled()
+        && gerritNetwork.getSpec().getIngress().getTls().isEnabled();
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ingress/GerritIngressReconciler.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ingress/GerritIngressReconciler.java
new file mode 100644
index 0000000..30c90a9
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ingress/GerritIngressReconciler.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ingress;
+
+import com.google.gerrit.k8s.operator.network.GerritClusterIngressCondition;
+import com.google.gerrit.k8s.operator.network.ingress.dependent.GerritClusterIngress;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import com.google.inject.Singleton;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
+import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
+
+@Singleton
+@ControllerConfiguration(
+    dependents = {
+      @Dependent(
+          name = "gerrit-ingress",
+          type = GerritClusterIngress.class,
+          reconcilePrecondition = GerritClusterIngressCondition.class)
+    })
+public class GerritIngressReconciler implements Reconciler<GerritNetwork> {
+
+  @Override
+  public UpdateControl<GerritNetwork> reconcile(
+      GerritNetwork resource, Context<GerritNetwork> context) throws Exception {
+    return UpdateControl.noUpdate();
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ingress/dependent/GerritClusterIngress.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ingress/dependent/GerritClusterIngress.java
new file mode 100644
index 0000000..a62e298
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ingress/dependent/GerritClusterIngress.java
@@ -0,0 +1,247 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ingress.dependent;
+
+import static com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork.SESSION_COOKIE_NAME;
+
+import com.google.gerrit.k8s.operator.gerrit.dependent.GerritService;
+import com.google.gerrit.k8s.operator.receiver.dependent.ReceiverService;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.fabric8.kubernetes.api.model.networking.v1.HTTPIngressPath;
+import io.fabric8.kubernetes.api.model.networking.v1.HTTPIngressPathBuilder;
+import io.fabric8.kubernetes.api.model.networking.v1.Ingress;
+import io.fabric8.kubernetes.api.model.networking.v1.IngressBuilder;
+import io.fabric8.kubernetes.api.model.networking.v1.IngressRule;
+import io.fabric8.kubernetes.api.model.networking.v1.IngressRuleBuilder;
+import io.fabric8.kubernetes.api.model.networking.v1.IngressSpecBuilder;
+import io.fabric8.kubernetes.api.model.networking.v1.IngressTLS;
+import io.fabric8.kubernetes.api.model.networking.v1.IngressTLSBuilder;
+import io.fabric8.kubernetes.api.model.networking.v1.ServiceBackendPort;
+import io.fabric8.kubernetes.api.model.networking.v1.ServiceBackendPortBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@KubernetesDependent
+public class GerritClusterIngress extends CRUDKubernetesDependentResource<Ingress, GerritNetwork> {
+  private static final String UPLOAD_PACK_URL_PATTERN = "/.*/git-upload-pack";
+  private static final String RECEIVE_PACK_URL_PATTERN = "/.*/git-receive-pack";
+  public static final String INGRESS_NAME = "gerrit-ingress";
+
+  public GerritClusterIngress() {
+    super(Ingress.class);
+  }
+
+  @Override
+  protected Ingress desired(GerritNetwork gerritNetwork, Context<GerritNetwork> context) {
+    IngressSpecBuilder ingressSpecBuilder =
+        new IngressSpecBuilder().withRules(getIngressRule(gerritNetwork));
+    if (gerritNetwork.getSpec().getIngress().getTls().isEnabled()) {
+      ingressSpecBuilder.withTls(getIngressTLS(gerritNetwork));
+    }
+
+    Ingress gerritIngress =
+        new IngressBuilder()
+            .withNewMetadata()
+            .withName("gerrit-ingress")
+            .withNamespace(gerritNetwork.getMetadata().getNamespace())
+            .withLabels(
+                GerritCluster.getLabels(
+                    gerritNetwork.getMetadata().getName(),
+                    "gerrit-ingress",
+                    this.getClass().getSimpleName()))
+            .withAnnotations(getAnnotations(gerritNetwork))
+            .endMetadata()
+            .withSpec(ingressSpecBuilder.build())
+            .build();
+
+    return gerritIngress;
+  }
+
+  private Map<String, String> getAnnotations(GerritNetwork gerritNetwork) {
+    Map<String, String> annotations = gerritNetwork.getSpec().getIngress().getAnnotations();
+    if (annotations == null) {
+      annotations = new HashMap<>();
+    }
+    annotations.put("nginx.ingress.kubernetes.io/use-regex", "true");
+    annotations.put("kubernetes.io/ingress.class", "nginx");
+
+    String configSnippet = "";
+    if (gerritNetwork.hasPrimaryGerrit() && gerritNetwork.hasGerritReplica()) {
+      String svcName = GerritService.getName(gerritNetwork.getSpec().getGerritReplica().getName());
+      configSnippet =
+          createNginxConfigSnippet(
+              "service=git-upload-pack", gerritNetwork.getMetadata().getNamespace(), svcName);
+    }
+    if (gerritNetwork.hasReceiver()) {
+      String svcName = ReceiverService.getName(gerritNetwork.getSpec().getReceiver().getName());
+      configSnippet =
+          createNginxConfigSnippet(
+              "service=git-receive-pack", gerritNetwork.getMetadata().getNamespace(), svcName);
+    }
+    if (!configSnippet.isBlank()) {
+      annotations.put("nginx.ingress.kubernetes.io/configuration-snippet", configSnippet);
+    }
+
+    annotations.put("nginx.ingress.kubernetes.io/affinity", "cookie");
+    annotations.put("nginx.ingress.kubernetes.io/session-cookie-name", SESSION_COOKIE_NAME);
+    annotations.put("nginx.ingress.kubernetes.io/session-cookie-path", "/");
+    annotations.put("nginx.ingress.kubernetes.io/session-cookie-max-age", "60");
+    annotations.put("nginx.ingress.kubernetes.io/session-cookie-expires", "60");
+
+    return annotations;
+  }
+
+  /**
+   * Creates a config snippet for the Nginx Ingress Controller [1]. This snippet will configure
+   * Nginx to route the request based on the `service` query parameter.
+   *
+   * <p>If it is set to `git-upload-pack` it will route the request to the provided service.
+   *
+   * <p>[1]https://docs.nginx.com/nginx-ingress-controller/configuration/ingress-resources/advanced-configuration-with-snippets/
+   *
+   * @param namespace Namespace of the destination service.
+   * @param svcName Name of the destination service.
+   * @return configuration snippet
+   */
+  private String createNginxConfigSnippet(String queryParam, String namespace, String svcName) {
+    StringBuilder configSnippet = new StringBuilder();
+    configSnippet.append("if ($args ~ ");
+    configSnippet.append(queryParam);
+    configSnippet.append("){");
+    configSnippet.append("\n");
+    configSnippet.append("  set $proxy_upstream_name \"");
+    configSnippet.append(namespace);
+    configSnippet.append("-");
+    configSnippet.append(svcName);
+    configSnippet.append("-");
+    configSnippet.append(GerritService.HTTP_PORT_NAME);
+    configSnippet.append("\";\n");
+    configSnippet.append("  set $proxy_host $proxy_upstream_name;");
+    configSnippet.append("\n");
+    configSnippet.append("  set $service_name \"");
+    configSnippet.append(svcName);
+    configSnippet.append("\";\n}");
+    return configSnippet.toString();
+  }
+
+  private IngressTLS getIngressTLS(GerritNetwork gerritNetwork) {
+    if (gerritNetwork.getSpec().getIngress().getTls().isEnabled()) {
+      return new IngressTLSBuilder()
+          .withHosts(gerritNetwork.getSpec().getIngress().getHost())
+          .withSecretName(gerritNetwork.getSpec().getIngress().getTls().getSecret())
+          .build();
+    }
+    return null;
+  }
+
+  private IngressRule getIngressRule(GerritNetwork gerritNetwork) {
+    List<HTTPIngressPath> ingressPaths = new ArrayList<>();
+    if (gerritNetwork.hasReceiver()) {
+      ingressPaths.addAll(getReceiverIngressPaths(gerritNetwork));
+    }
+    if (gerritNetwork.hasGerrits()) {
+      ingressPaths.addAll(getGerritHTTPIngressPaths(gerritNetwork));
+    }
+
+    if (ingressPaths.isEmpty()) {
+      throw new IllegalStateException(
+          "Failed to create Ingress: No Receiver or Gerrit in GerritCluster.");
+    }
+
+    return new IngressRuleBuilder()
+        .withHost(gerritNetwork.getSpec().getIngress().getHost())
+        .withNewHttp()
+        .withPaths(ingressPaths)
+        .endHttp()
+        .build();
+  }
+
+  private List<HTTPIngressPath> getGerritHTTPIngressPaths(GerritNetwork gerritNetwork) {
+    ServiceBackendPort port =
+        new ServiceBackendPortBuilder().withName(GerritService.HTTP_PORT_NAME).build();
+
+    List<HTTPIngressPath> paths = new ArrayList<>();
+    // Order matters, since routing rules will be applied in order!
+    if (!gerritNetwork.hasPrimaryGerrit() && gerritNetwork.hasGerritReplica()) {
+      paths.add(
+          new HTTPIngressPathBuilder()
+              .withPathType("Prefix")
+              .withPath("/")
+              .withNewBackend()
+              .withNewService()
+              .withName(GerritService.getName(gerritNetwork.getSpec().getGerritReplica().getName()))
+              .withPort(port)
+              .endService()
+              .endBackend()
+              .build());
+      return paths;
+    }
+    if (gerritNetwork.hasGerritReplica()) {
+      paths.add(
+          new HTTPIngressPathBuilder()
+              .withPathType("Prefix")
+              .withPath(UPLOAD_PACK_URL_PATTERN)
+              .withNewBackend()
+              .withNewService()
+              .withName(GerritService.getName(gerritNetwork.getSpec().getGerritReplica().getName()))
+              .withPort(port)
+              .endService()
+              .endBackend()
+              .build());
+    }
+    if (gerritNetwork.hasPrimaryGerrit()) {
+      paths.add(
+          new HTTPIngressPathBuilder()
+              .withPathType("Prefix")
+              .withPath("/")
+              .withNewBackend()
+              .withNewService()
+              .withName(GerritService.getName(gerritNetwork.getSpec().getPrimaryGerrit().getName()))
+              .withPort(port)
+              .endService()
+              .endBackend()
+              .build());
+    }
+    return paths;
+  }
+
+  private List<HTTPIngressPath> getReceiverIngressPaths(GerritNetwork gerritNetwork) {
+    String svcName = ReceiverService.getName(gerritNetwork.getSpec().getReceiver().getName());
+    List<HTTPIngressPath> paths = new ArrayList<>();
+    ServiceBackendPort port =
+        new ServiceBackendPortBuilder().withName(ReceiverService.HTTP_PORT_NAME).build();
+
+    for (String path : List.of("/a/projects", RECEIVE_PACK_URL_PATTERN)) {
+      paths.add(
+          new HTTPIngressPathBuilder()
+              .withPathType("Prefix")
+              .withPath(path)
+              .withNewBackend()
+              .withNewService()
+              .withName(svcName)
+              .withPort(port)
+              .endService()
+              .endBackend()
+              .build());
+    }
+    return paths;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/GerritIstioReconciler.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/GerritIstioReconciler.java
new file mode 100644
index 0000000..4d12941
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/GerritIstioReconciler.java
@@ -0,0 +1,89 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.istio;
+
+import static com.google.gerrit.k8s.operator.network.istio.GerritIstioReconciler.ISTIO_DESTINATION_RULE_EVENT_SOURCE;
+import static com.google.gerrit.k8s.operator.network.istio.GerritIstioReconciler.ISTIO_VIRTUAL_SERVICE_EVENT_SOURCE;
+
+import com.google.gerrit.k8s.operator.network.GerritClusterIngressCondition;
+import com.google.gerrit.k8s.operator.network.istio.dependent.GerritClusterIstioGateway;
+import com.google.gerrit.k8s.operator.network.istio.dependent.GerritIstioCondition;
+import com.google.gerrit.k8s.operator.network.istio.dependent.GerritIstioDestinationRule;
+import com.google.gerrit.k8s.operator.network.istio.dependent.GerritIstioVirtualService;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import com.google.inject.Singleton;
+import io.fabric8.istio.api.networking.v1beta1.DestinationRule;
+import io.fabric8.istio.api.networking.v1beta1.VirtualService;
+import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
+import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer;
+import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
+import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
+import io.javaoperatorsdk.operator.processing.event.source.EventSource;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.HashMap;
+import java.util.Map;
+
+@Singleton
+@ControllerConfiguration(
+    dependents = {
+      @Dependent(
+          name = "gerrit-destination-rules",
+          type = GerritIstioDestinationRule.class,
+          reconcilePrecondition = GerritIstioCondition.class,
+          useEventSourceWithName = ISTIO_DESTINATION_RULE_EVENT_SOURCE),
+      @Dependent(
+          name = "gerrit-istio-gateway",
+          type = GerritClusterIstioGateway.class,
+          reconcilePrecondition = GerritClusterIngressCondition.class),
+      @Dependent(
+          name = "gerrit-istio-virtual-service",
+          type = GerritIstioVirtualService.class,
+          reconcilePrecondition = GerritIstioCondition.class,
+          dependsOn = {"gerrit-istio-gateway"},
+          useEventSourceWithName = ISTIO_VIRTUAL_SERVICE_EVENT_SOURCE),
+    })
+public class GerritIstioReconciler
+    implements Reconciler<GerritNetwork>, EventSourceInitializer<GerritNetwork> {
+  public static final String ISTIO_DESTINATION_RULE_EVENT_SOURCE =
+      "gerrit-cluster-istio-destination-rule";
+  public static final String ISTIO_VIRTUAL_SERVICE_EVENT_SOURCE =
+      "gerrit-cluster-istio-virtual-service";
+
+  @Override
+  public Map<String, EventSource> prepareEventSources(EventSourceContext<GerritNetwork> context) {
+    InformerEventSource<DestinationRule, GerritNetwork> gerritIstioDestinationRuleEventSource =
+        new InformerEventSource<>(
+            InformerConfiguration.from(DestinationRule.class, context).build(), context);
+
+    InformerEventSource<VirtualService, GerritNetwork> virtualServiceEventSource =
+        new InformerEventSource<>(
+            InformerConfiguration.from(VirtualService.class, context).build(), context);
+
+    Map<String, EventSource> eventSources = new HashMap<>();
+    eventSources.put(ISTIO_DESTINATION_RULE_EVENT_SOURCE, gerritIstioDestinationRuleEventSource);
+    eventSources.put(ISTIO_VIRTUAL_SERVICE_EVENT_SOURCE, virtualServiceEventSource);
+    return eventSources;
+  }
+
+  @Override
+  public UpdateControl<GerritNetwork> reconcile(
+      GerritNetwork resource, Context<GerritNetwork> context) throws Exception {
+    return UpdateControl.noUpdate();
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritClusterIstioGateway.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritClusterIstioGateway.java
new file mode 100644
index 0000000..099a4ed
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritClusterIstioGateway.java
@@ -0,0 +1,115 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.istio.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.fabric8.istio.api.networking.v1beta1.Gateway;
+import io.fabric8.istio.api.networking.v1beta1.GatewayBuilder;
+import io.fabric8.istio.api.networking.v1beta1.Server;
+import io.fabric8.istio.api.networking.v1beta1.ServerBuilder;
+import io.fabric8.istio.api.networking.v1beta1.ServerTLSSettingsTLSmode;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class GerritClusterIstioGateway
+    extends CRUDKubernetesDependentResource<Gateway, GerritNetwork> {
+  public static final String NAME = "gerrit-istio-gateway";
+
+  public GerritClusterIstioGateway() {
+    super(Gateway.class);
+  }
+
+  @Override
+  public Gateway desired(GerritNetwork gerritNetwork, Context<GerritNetwork> context) {
+    return new GatewayBuilder()
+        .withNewMetadata()
+        .withName(NAME)
+        .withNamespace(gerritNetwork.getMetadata().getNamespace())
+        .withLabels(
+            GerritCluster.getLabels(
+                gerritNetwork.getMetadata().getName(), NAME, this.getClass().getSimpleName()))
+        .endMetadata()
+        .withNewSpec()
+        .withSelector(Map.of("istio", "ingressgateway"))
+        .withServers(configureServers(gerritNetwork))
+        .endSpec()
+        .build();
+  }
+
+  private List<Server> configureServers(GerritNetwork gerritNetwork) {
+    List<Server> servers = new ArrayList<>();
+    String gerritClusterHost = gerritNetwork.getSpec().getIngress().getHost();
+
+    servers.add(
+        new ServerBuilder()
+            .withNewPort()
+            .withName("http")
+            .withNumber(80)
+            .withProtocol("HTTP")
+            .endPort()
+            .withHosts(gerritClusterHost)
+            .withNewTls()
+            .withHttpsRedirect(gerritNetwork.getSpec().getIngress().getTls().isEnabled())
+            .endTls()
+            .build());
+
+    if (gerritNetwork.getSpec().getIngress().getTls().isEnabled()) {
+      servers.add(
+          new ServerBuilder()
+              .withNewPort()
+              .withName("https")
+              .withNumber(443)
+              .withProtocol("HTTPS")
+              .endPort()
+              .withHosts(gerritClusterHost)
+              .withNewTls()
+              .withMode(ServerTLSSettingsTLSmode.SIMPLE)
+              .withCredentialName(gerritNetwork.getSpec().getIngress().getTls().getSecret())
+              .endTls()
+              .build());
+    }
+
+    if (gerritNetwork.getSpec().getIngress().getSsh().isEnabled() && gerritNetwork.hasGerrits()) {
+      if (gerritNetwork.hasPrimaryGerrit()) {
+        servers.add(
+            new ServerBuilder()
+                .withNewPort()
+                .withName("ssh-primary")
+                .withNumber(gerritNetwork.getSpec().getPrimaryGerrit().getSshPort())
+                .withProtocol("TCP")
+                .endPort()
+                .withHosts(gerritClusterHost)
+                .build());
+      }
+      if (gerritNetwork.hasGerritReplica()) {
+        servers.add(
+            new ServerBuilder()
+                .withNewPort()
+                .withName("ssh-replica")
+                .withNumber(gerritNetwork.getSpec().getGerritReplica().getSshPort())
+                .withProtocol("TCP")
+                .endPort()
+                .withHosts(gerritClusterHost)
+                .build());
+      }
+    }
+
+    return servers;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritIstioCondition.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritIstioCondition.java
new file mode 100644
index 0000000..ac6c6c3
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritIstioCondition.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.istio.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.fabric8.istio.api.networking.v1beta1.VirtualService;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
+
+public class GerritIstioCondition implements Condition<VirtualService, GerritNetwork> {
+
+  @Override
+  public boolean isMet(
+      DependentResource<VirtualService, GerritNetwork> dependentResource,
+      GerritNetwork gerritNetwork,
+      Context<GerritNetwork> context) {
+
+    return gerritNetwork.getSpec().getIngress().isEnabled()
+        && (gerritNetwork.hasGerrits() || gerritNetwork.hasReceiver());
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritIstioDestinationRule.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritIstioDestinationRule.java
new file mode 100644
index 0000000..98f8267
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritIstioDestinationRule.java
@@ -0,0 +1,131 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.istio.dependent;
+
+import static com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork.SESSION_COOKIE_NAME;
+import static com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork.SESSION_COOKIE_TTL;
+
+import com.google.gerrit.k8s.operator.gerrit.dependent.GerritService;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplate;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.fabric8.istio.api.networking.v1beta1.DestinationRule;
+import io.fabric8.istio.api.networking.v1beta1.DestinationRuleBuilder;
+import io.fabric8.istio.api.networking.v1beta1.LoadBalancerSettingsSimpleLB;
+import io.fabric8.istio.api.networking.v1beta1.TrafficPolicy;
+import io.fabric8.istio.api.networking.v1beta1.TrafficPolicyBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected;
+import io.javaoperatorsdk.operator.processing.dependent.BulkDependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.Creator;
+import io.javaoperatorsdk.operator.processing.dependent.Updater;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+public class GerritIstioDestinationRule
+    extends KubernetesDependentResource<DestinationRule, GerritNetwork>
+    implements Creator<DestinationRule, GerritNetwork>,
+        Updater<DestinationRule, GerritNetwork>,
+        Deleter<GerritNetwork>,
+        BulkDependentResource<DestinationRule, GerritNetwork>,
+        GarbageCollected<GerritNetwork> {
+
+  public GerritIstioDestinationRule() {
+    super(DestinationRule.class);
+  }
+
+  protected DestinationRule desired(
+      GerritNetwork gerritNetwork, String gerritName, boolean isReplica) {
+
+    return new DestinationRuleBuilder()
+        .withNewMetadata()
+        .withName(getName(gerritName))
+        .withNamespace(gerritNetwork.getMetadata().getNamespace())
+        .withLabels(
+            GerritCluster.getLabels(
+                gerritNetwork.getMetadata().getName(),
+                getName(gerritName),
+                this.getClass().getSimpleName()))
+        .endMetadata()
+        .withNewSpec()
+        .withHost(GerritService.getHostname(gerritName, gerritNetwork.getMetadata().getNamespace()))
+        .withTrafficPolicy(getTrafficPolicy(isReplica))
+        .endSpec()
+        .build();
+  }
+
+  private TrafficPolicy getTrafficPolicy(boolean isReplica) {
+    if (isReplica) {
+      return new TrafficPolicyBuilder()
+          .withNewLoadBalancer()
+          .withNewLoadBalancerSettingsSimpleLbPolicy()
+          .withSimple(LoadBalancerSettingsSimpleLB.LEAST_CONN)
+          .endLoadBalancerSettingsSimpleLbPolicy()
+          .endLoadBalancer()
+          .build();
+    }
+    return new TrafficPolicyBuilder()
+        .withNewLoadBalancer()
+        .withNewLoadBalancerSettingsConsistentHashLbPolicy()
+        .withNewConsistentHash()
+        .withNewLoadBalancerSettingsConsistentHashLBHttpCookieKey()
+        .withNewHttpCookie()
+        .withName(SESSION_COOKIE_NAME)
+        .withTtl(SESSION_COOKIE_TTL)
+        .endHttpCookie()
+        .endLoadBalancerSettingsConsistentHashLBHttpCookieKey()
+        .endConsistentHash()
+        .endLoadBalancerSettingsConsistentHashLbPolicy()
+        .endLoadBalancer()
+        .build();
+  }
+
+  public static String getName(GerritTemplate gerrit) {
+    return gerrit.getMetadata().getName();
+  }
+
+  public static String getName(String gerritName) {
+    return gerritName;
+  }
+
+  @Override
+  public Map<String, DestinationRule> desiredResources(
+      GerritNetwork gerritNetwork, Context<GerritNetwork> context) {
+    Map<String, DestinationRule> drs = new HashMap<>();
+    if (gerritNetwork.hasPrimaryGerrit()) {
+      String primaryGerritName = gerritNetwork.getSpec().getPrimaryGerrit().getName();
+      drs.put(primaryGerritName, desired(gerritNetwork, primaryGerritName, false));
+    }
+    if (gerritNetwork.hasGerritReplica()) {
+      String gerritReplicaName = gerritNetwork.getSpec().getGerritReplica().getName();
+      drs.put(gerritReplicaName, desired(gerritNetwork, gerritReplicaName, true));
+    }
+    return drs;
+  }
+
+  @Override
+  public Map<String, DestinationRule> getSecondaryResources(
+      GerritNetwork gerritNetwork, Context<GerritNetwork> context) {
+    Set<DestinationRule> drs = context.getSecondaryResources(DestinationRule.class);
+    Map<String, DestinationRule> result = new HashMap<>(drs.size());
+    for (DestinationRule dr : drs) {
+      result.put(dr.getMetadata().getName(), dr);
+    }
+    return result;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritIstioVirtualService.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritIstioVirtualService.java
new file mode 100644
index 0000000..f0d683c
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritIstioVirtualService.java
@@ -0,0 +1,238 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.istio.dependent;
+
+import com.google.gerrit.k8s.operator.gerrit.dependent.GerritService;
+import com.google.gerrit.k8s.operator.receiver.dependent.ReceiverService;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.NetworkMember;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.NetworkMemberWithSsh;
+import io.fabric8.istio.api.networking.v1beta1.HTTPMatchRequest;
+import io.fabric8.istio.api.networking.v1beta1.HTTPMatchRequestBuilder;
+import io.fabric8.istio.api.networking.v1beta1.HTTPRoute;
+import io.fabric8.istio.api.networking.v1beta1.HTTPRouteBuilder;
+import io.fabric8.istio.api.networking.v1beta1.HTTPRouteDestination;
+import io.fabric8.istio.api.networking.v1beta1.HTTPRouteDestinationBuilder;
+import io.fabric8.istio.api.networking.v1beta1.L4MatchAttributesBuilder;
+import io.fabric8.istio.api.networking.v1beta1.RouteDestination;
+import io.fabric8.istio.api.networking.v1beta1.RouteDestinationBuilder;
+import io.fabric8.istio.api.networking.v1beta1.StringMatchBuilder;
+import io.fabric8.istio.api.networking.v1beta1.TCPRoute;
+import io.fabric8.istio.api.networking.v1beta1.TCPRouteBuilder;
+import io.fabric8.istio.api.networking.v1beta1.VirtualService;
+import io.fabric8.istio.api.networking.v1beta1.VirtualServiceBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+@KubernetesDependent
+public class GerritIstioVirtualService
+    extends CRUDKubernetesDependentResource<VirtualService, GerritNetwork> {
+  private static final String INFO_REF_URL_PATTERN = "^/(.*)/info/refs$";
+  private static final String UPLOAD_PACK_URL_PATTERN = "^/(.*)/git-upload-pack$";
+  private static final String RECEIVE_PACK_URL_PATTERN = "^/(.*)/git-receive-pack$";
+  public static final String NAME_SUFFIX = "gerrit-http-virtual-service";
+
+  public GerritIstioVirtualService() {
+    super(VirtualService.class);
+  }
+
+  @Override
+  protected VirtualService desired(GerritNetwork gerritNetwork, Context<GerritNetwork> context) {
+    String gerritClusterHost = gerritNetwork.getSpec().getIngress().getHost();
+    String namespace = gerritNetwork.getMetadata().getNamespace();
+
+    return new VirtualServiceBuilder()
+        .withNewMetadata()
+        .withName(gerritNetwork.getDependentResourceName(NAME_SUFFIX))
+        .withNamespace(namespace)
+        .withLabels(
+            GerritCluster.getLabels(
+                gerritNetwork.getMetadata().getName(),
+                gerritNetwork.getDependentResourceName(NAME_SUFFIX),
+                this.getClass().getSimpleName()))
+        .endMetadata()
+        .withNewSpec()
+        .withHosts(gerritClusterHost)
+        .withGateways(namespace + "/" + GerritClusterIstioGateway.NAME)
+        .withHttp(getHTTPRoutes(gerritNetwork))
+        .withTcp(getTCPRoutes(gerritNetwork))
+        .endSpec()
+        .build();
+  }
+
+  private List<HTTPRoute> getHTTPRoutes(GerritNetwork gerritNetwork) {
+    String namespace = gerritNetwork.getMetadata().getNamespace();
+    List<HTTPRoute> routes = new ArrayList<>();
+    if (gerritNetwork.hasReceiver()) {
+      routes.add(
+          new HTTPRouteBuilder()
+              .withName("receiver-" + gerritNetwork.getSpec().getReceiver().getName())
+              .withMatch(getReceiverMatches())
+              .withRoute(
+                  getReceiverHTTPDestination(gerritNetwork.getSpec().getReceiver(), namespace))
+              .build());
+    }
+    if (gerritNetwork.hasGerritReplica()) {
+      HTTPRouteBuilder routeBuilder =
+          new HTTPRouteBuilder()
+              .withName("gerrit-replica-" + gerritNetwork.getSpec().getGerritReplica().getName());
+      if (gerritNetwork.hasPrimaryGerrit()) {
+        routeBuilder = routeBuilder.withMatch(getGerritReplicaMatches());
+      }
+      routes.add(
+          routeBuilder
+              .withRoute(
+                  getGerritHTTPDestinations(gerritNetwork.getSpec().getGerritReplica(), namespace))
+              .build());
+    }
+    if (gerritNetwork.hasPrimaryGerrit()) {
+      routes.add(
+          new HTTPRouteBuilder()
+              .withName("gerrit-primary-" + gerritNetwork.getSpec().getPrimaryGerrit().getName())
+              .withRoute(
+                  getGerritHTTPDestinations(gerritNetwork.getSpec().getPrimaryGerrit(), namespace))
+              .build());
+    }
+
+    return routes;
+  }
+
+  private HTTPRouteDestination getGerritHTTPDestinations(
+      NetworkMemberWithSsh networkMember, String namespace) {
+    return new HTTPRouteDestinationBuilder()
+        .withNewDestination()
+        .withHost(GerritService.getHostname(networkMember.getName(), namespace))
+        .withNewPort()
+        .withNumber(networkMember.getHttpPort())
+        .endPort()
+        .endDestination()
+        .build();
+  }
+
+  private List<HTTPMatchRequest> getGerritReplicaMatches() {
+    List<HTTPMatchRequest> matches = new ArrayList<>();
+    matches.add(
+        new HTTPMatchRequestBuilder()
+            .withNewUri()
+            .withNewStringMatchRegexType()
+            .withRegex(INFO_REF_URL_PATTERN)
+            .endStringMatchRegexType()
+            .endUri()
+            .withQueryParams(
+                Map.of(
+                    "service",
+                    new StringMatchBuilder()
+                        .withNewStringMatchExactType("git-upload-pack")
+                        .build()))
+            .withIgnoreUriCase()
+            .withNewMethod()
+            .withNewStringMatchExactType()
+            .withExact("GET")
+            .endStringMatchExactType()
+            .endMethod()
+            .build());
+    matches.add(
+        new HTTPMatchRequestBuilder()
+            .withNewUri()
+            .withNewStringMatchRegexType()
+            .withRegex(UPLOAD_PACK_URL_PATTERN)
+            .endStringMatchRegexType()
+            .endUri()
+            .withIgnoreUriCase()
+            .withNewMethod()
+            .withNewStringMatchExactType()
+            .withExact("POST")
+            .endStringMatchExactType()
+            .endMethod()
+            .build());
+    return matches;
+  }
+
+  private HTTPRouteDestination getReceiverHTTPDestination(
+      NetworkMember receiver, String namespace) {
+    return new HTTPRouteDestinationBuilder()
+        .withNewDestination()
+        .withHost(ReceiverService.getHostname(receiver.getName(), namespace))
+        .withNewPort()
+        .withNumber(receiver.getHttpPort())
+        .endPort()
+        .endDestination()
+        .build();
+  }
+
+  private List<HTTPMatchRequest> getReceiverMatches() {
+    List<HTTPMatchRequest> matches = new ArrayList<>();
+    matches.add(
+        new HTTPMatchRequestBuilder()
+            .withUri(new StringMatchBuilder().withNewStringMatchPrefixType("/a/projects/").build())
+            .build());
+    matches.add(
+        new HTTPMatchRequestBuilder()
+            .withNewUri()
+            .withNewStringMatchRegexType()
+            .withRegex(RECEIVE_PACK_URL_PATTERN)
+            .endStringMatchRegexType()
+            .endUri()
+            .build());
+    matches.add(
+        new HTTPMatchRequestBuilder()
+            .withNewUri()
+            .withNewStringMatchRegexType()
+            .withRegex(INFO_REF_URL_PATTERN)
+            .endStringMatchRegexType()
+            .endUri()
+            .withQueryParams(
+                Map.of(
+                    "service",
+                    new StringMatchBuilder()
+                        .withNewStringMatchExactType("git-receive-pack")
+                        .build()))
+            .build());
+    return matches;
+  }
+
+  private List<TCPRoute> getTCPRoutes(GerritNetwork gerritNetwork) {
+    List<TCPRoute> routes = new ArrayList<>();
+    for (NetworkMemberWithSsh gerrit : gerritNetwork.getSpec().getGerrits()) {
+      if (gerritNetwork.getSpec().getIngress().getSsh().isEnabled() && gerrit.getSshPort() > 0) {
+        routes.add(
+            new TCPRouteBuilder()
+                .withMatch(
+                    List.of(new L4MatchAttributesBuilder().withPort(gerrit.getSshPort()).build()))
+                .withRoute(
+                    getGerritTCPDestination(gerrit, gerritNetwork.getMetadata().getNamespace()))
+                .build());
+      }
+    }
+    return routes;
+  }
+
+  private RouteDestination getGerritTCPDestination(
+      NetworkMemberWithSsh networkMember, String namespace) {
+    return new RouteDestinationBuilder()
+        .withNewDestination()
+        .withHost(GerritService.getHostname(networkMember.getName(), namespace))
+        .withNewPort()
+        .withNumber(networkMember.getSshPort())
+        .endPort()
+        .endDestination()
+        .build();
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/none/GerritNoIngressReconciler.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/none/GerritNoIngressReconciler.java
new file mode 100644
index 0000000..2179260
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/none/GerritNoIngressReconciler.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.none;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import com.google.inject.Singleton;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
+import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
+
+@Singleton
+@ControllerConfiguration
+public class GerritNoIngressReconciler implements Reconciler<GerritNetwork> {
+
+  @Override
+  public UpdateControl<GerritNetwork> reconcile(
+      GerritNetwork resource, Context<GerritNetwork> context) throws Exception {
+    return UpdateControl.noUpdate();
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/ReceiverReconciler.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/ReceiverReconciler.java
new file mode 100644
index 0000000..b48a05f
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/ReceiverReconciler.java
@@ -0,0 +1,149 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.receiver;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.k8s.operator.receiver.dependent.ReceiverDeployment;
+import com.google.gerrit.k8s.operator.receiver.dependent.ReceiverService;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.Receiver;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.ReceiverStatus;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import io.fabric8.kubernetes.api.model.Secret;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
+import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer;
+import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
+import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.EventSource;
+import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Singleton
+@ControllerConfiguration(
+    dependents = {
+      @Dependent(name = "receiver-deployment", type = ReceiverDeployment.class),
+      @Dependent(
+          name = "receiver-service",
+          type = ReceiverService.class,
+          dependsOn = {"receiver-deployment"})
+    })
+public class ReceiverReconciler implements Reconciler<Receiver>, EventSourceInitializer<Receiver> {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  private static final String SECRET_EVENT_SOURCE_NAME = "secret-event-source";
+  private final KubernetesClient client;
+
+  @Inject
+  public ReceiverReconciler(KubernetesClient client) {
+    this.client = client;
+  }
+
+  @Override
+  public Map<String, EventSource> prepareEventSources(EventSourceContext<Receiver> context) {
+    final SecondaryToPrimaryMapper<Secret> secretMapper =
+        (Secret secret) ->
+            context
+                .getPrimaryCache()
+                .list(
+                    receiver ->
+                        receiver
+                            .getSpec()
+                            .getCredentialSecretRef()
+                            .equals(secret.getMetadata().getName()))
+                .map(ResourceID::fromResource)
+                .collect(Collectors.toSet());
+
+    InformerEventSource<Secret, Receiver> secretEventSource =
+        new InformerEventSource<>(
+            InformerConfiguration.from(Secret.class, context)
+                .withSecondaryToPrimaryMapper(secretMapper)
+                .build(),
+            context);
+
+    Map<String, EventSource> eventSources = new HashMap<>();
+    eventSources.put(SECRET_EVENT_SOURCE_NAME, secretEventSource);
+    return eventSources;
+  }
+
+  @Override
+  public UpdateControl<Receiver> reconcile(Receiver receiver, Context<Receiver> context)
+      throws Exception {
+    if (receiver.getStatus() != null && isReceiverRestartRequired(receiver, context)) {
+      restartReceiverDeployment(receiver);
+    }
+
+    return UpdateControl.patchStatus(updateStatus(receiver, context));
+  }
+
+  void restartReceiverDeployment(Receiver receiver) {
+    logger.atInfo().log(
+        "Restarting Receiver %s due to configuration change.", receiver.getMetadata().getName());
+    client
+        .apps()
+        .deployments()
+        .inNamespace(receiver.getMetadata().getNamespace())
+        .withName(receiver.getMetadata().getName())
+        .rolling()
+        .restart();
+  }
+
+  private Receiver updateStatus(Receiver receiver, Context<Receiver> context) {
+    ReceiverStatus status = receiver.getStatus();
+    if (status == null) {
+      status = new ReceiverStatus();
+    }
+
+    Secret sec =
+        client
+            .secrets()
+            .inNamespace(receiver.getMetadata().getNamespace())
+            .withName(receiver.getSpec().getCredentialSecretRef())
+            .get();
+
+    if (sec != null) {
+      status.setAppliedCredentialSecretVersion(sec.getMetadata().getResourceVersion());
+    }
+
+    receiver.setStatus(status);
+    return receiver;
+  }
+
+  private boolean isReceiverRestartRequired(Receiver receiver, Context<Receiver> context) {
+    String secVersion =
+        client
+            .secrets()
+            .inNamespace(receiver.getMetadata().getNamespace())
+            .withName(receiver.getSpec().getCredentialSecretRef())
+            .get()
+            .getMetadata()
+            .getResourceVersion();
+    String appliedSecVersion = receiver.getStatus().getAppliedCredentialSecretVersion();
+    if (!secVersion.equals(appliedSecVersion)) {
+      logger.atFine().log(
+          "Looking up Secret: %s; Installed secret resource version: %s; Resource version known to the Receiver: %s",
+          receiver.getSpec().getCredentialSecretRef(), secVersion, appliedSecVersion);
+      return true;
+    }
+    return false;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/dependent/ReceiverDeployment.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/dependent/ReceiverDeployment.java
new file mode 100644
index 0000000..16c0072
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/dependent/ReceiverDeployment.java
@@ -0,0 +1,188 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.receiver.dependent;
+
+import com.google.gerrit.k8s.operator.receiver.ReceiverReconciler;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.Receiver;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.NfsWorkaroundConfig;
+import io.fabric8.kubernetes.api.model.Container;
+import io.fabric8.kubernetes.api.model.ContainerPort;
+import io.fabric8.kubernetes.api.model.Volume;
+import io.fabric8.kubernetes.api.model.VolumeBuilder;
+import io.fabric8.kubernetes.api.model.VolumeMount;
+import io.fabric8.kubernetes.api.model.VolumeMountBuilder;
+import io.fabric8.kubernetes.api.model.apps.Deployment;
+import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+@KubernetesDependent
+public class ReceiverDeployment extends CRUDKubernetesDependentResource<Deployment, Receiver> {
+  public static final int HTTP_PORT = 80;
+
+  public ReceiverDeployment() {
+    super(Deployment.class);
+  }
+
+  @Override
+  protected Deployment desired(Receiver receiver, Context<Receiver> context) {
+    DeploymentBuilder deploymentBuilder = new DeploymentBuilder();
+
+    List<Container> initContainers = new ArrayList<>();
+
+    NfsWorkaroundConfig nfsWorkaround =
+        receiver.getSpec().getStorage().getStorageClasses().getNfsWorkaround();
+    if (nfsWorkaround.isEnabled() && nfsWorkaround.isChownOnStartup()) {
+      initContainers.add(
+          GerritCluster.createNfsInitContainer(
+              receiver
+                      .getSpec()
+                      .getStorage()
+                      .getStorageClasses()
+                      .getNfsWorkaround()
+                      .getIdmapdConfig()
+                  != null,
+              receiver.getSpec().getContainerImages()));
+    }
+
+    deploymentBuilder
+        .withApiVersion("apps/v1")
+        .withNewMetadata()
+        .withName(receiver.getMetadata().getName())
+        .withNamespace(receiver.getMetadata().getNamespace())
+        .withLabels(getLabels(receiver))
+        .endMetadata()
+        .withNewSpec()
+        .withReplicas(receiver.getSpec().getReplicas())
+        .withNewStrategy()
+        .withNewRollingUpdate()
+        .withMaxSurge(receiver.getSpec().getMaxSurge())
+        .withMaxUnavailable(receiver.getSpec().getMaxUnavailable())
+        .endRollingUpdate()
+        .endStrategy()
+        .withNewSelector()
+        .withMatchLabels(getSelectorLabels(receiver))
+        .endSelector()
+        .withNewTemplate()
+        .withNewMetadata()
+        .withLabels(getLabels(receiver))
+        .endMetadata()
+        .withNewSpec()
+        .withTolerations(receiver.getSpec().getTolerations())
+        .withTopologySpreadConstraints(receiver.getSpec().getTopologySpreadConstraints())
+        .withAffinity(receiver.getSpec().getAffinity())
+        .withPriorityClassName(receiver.getSpec().getPriorityClassName())
+        .addAllToImagePullSecrets(receiver.getSpec().getContainerImages().getImagePullSecrets())
+        .withNewSecurityContext()
+        .withFsGroup(100L)
+        .endSecurityContext()
+        .addAllToInitContainers(initContainers)
+        .addNewContainer()
+        .withName("apache-git-http-backend")
+        .withImagePullPolicy(receiver.getSpec().getContainerImages().getImagePullPolicy())
+        .withImage(
+            receiver
+                .getSpec()
+                .getContainerImages()
+                .getGerritImages()
+                .getFullImageName("apache-git-http-backend"))
+        .withEnv(GerritCluster.getPodNameEnvVar())
+        .withPorts(getContainerPorts(receiver))
+        .withResources(receiver.getSpec().getResources())
+        .withReadinessProbe(receiver.getSpec().getReadinessProbe())
+        .withLivenessProbe(receiver.getSpec().getLivenessProbe())
+        .addAllToVolumeMounts(getVolumeMounts(receiver, false))
+        .endContainer()
+        .addAllToVolumes(getVolumes(receiver))
+        .endSpec()
+        .endTemplate()
+        .endSpec();
+
+    return deploymentBuilder.build();
+  }
+
+  private static String getComponentName(Receiver receiver) {
+    return String.format("receiver-deployment-%s", receiver.getMetadata().getName());
+  }
+
+  public static Map<String, String> getSelectorLabels(Receiver receiver) {
+    return GerritCluster.getSelectorLabels(
+        receiver.getMetadata().getName(), getComponentName(receiver));
+  }
+
+  public static Map<String, String> getLabels(Receiver receiver) {
+    return GerritCluster.getLabels(
+        receiver.getMetadata().getName(),
+        getComponentName(receiver),
+        ReceiverReconciler.class.getSimpleName());
+  }
+
+  private Set<Volume> getVolumes(Receiver receiver) {
+    Set<Volume> volumes = new HashSet<>();
+    volumes.add(
+        GerritCluster.getSharedVolume(
+            receiver.getSpec().getStorage().getSharedStorage().getExternalPVC()));
+
+    volumes.add(
+        new VolumeBuilder()
+            .withName(receiver.getSpec().getCredentialSecretRef())
+            .withNewSecret()
+            .withSecretName(receiver.getSpec().getCredentialSecretRef())
+            .endSecret()
+            .build());
+
+    NfsWorkaroundConfig nfsWorkaround =
+        receiver.getSpec().getStorage().getStorageClasses().getNfsWorkaround();
+    if (nfsWorkaround.isEnabled() && nfsWorkaround.getIdmapdConfig() != null) {
+      volumes.add(GerritCluster.getNfsImapdConfigVolume());
+    }
+
+    return volumes;
+  }
+
+  private Set<VolumeMount> getVolumeMounts(Receiver receiver, boolean isInitContainer) {
+    Set<VolumeMount> volumeMounts = new HashSet<>();
+    volumeMounts.add(GerritCluster.getGitRepositoriesVolumeMount("/var/gerrit/git"));
+    volumeMounts.add(GerritCluster.getLogsVolumeMount("/var/log/apache2"));
+
+    volumeMounts.add(
+        new VolumeMountBuilder()
+            .withName(receiver.getSpec().getCredentialSecretRef())
+            .withMountPath("/var/apache/credentials/.htpasswd")
+            .withSubPath(".htpasswd")
+            .build());
+
+    NfsWorkaroundConfig nfsWorkaround =
+        receiver.getSpec().getStorage().getStorageClasses().getNfsWorkaround();
+    if (nfsWorkaround.isEnabled() && nfsWorkaround.getIdmapdConfig() != null) {
+      volumeMounts.add(GerritCluster.getNfsImapdConfigVolumeMount());
+    }
+
+    return volumeMounts;
+  }
+
+  private List<ContainerPort> getContainerPorts(Receiver receiver) {
+    List<ContainerPort> containerPorts = new ArrayList<>();
+    containerPorts.add(new ContainerPort(HTTP_PORT, null, null, "http", null));
+    return containerPorts;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/dependent/ReceiverSecret.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/dependent/ReceiverSecret.java
new file mode 100644
index 0000000..ca4700a
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/dependent/ReceiverSecret.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.receiver.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.Receiver;
+import io.fabric8.kubernetes.api.model.Secret;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@KubernetesDependent
+public class ReceiverSecret extends KubernetesDependentResource<Secret, Receiver>
+    implements SecondaryToPrimaryMapper<Secret> {
+  public ReceiverSecret() {
+    super(Secret.class);
+  }
+
+  @Override
+  public Set<ResourceID> toPrimaryResourceIDs(Secret secret) {
+    return client
+        .resources(Receiver.class)
+        .inNamespace(secret.getMetadata().getNamespace())
+        .list()
+        .getItems()
+        .stream()
+        .filter(g -> g.getSpec().getCredentialSecretRef().equals(secret.getMetadata().getName()))
+        .map(g -> ResourceID.fromResource(g))
+        .collect(Collectors.toSet());
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/dependent/ReceiverService.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/dependent/ReceiverService.java
new file mode 100644
index 0000000..3ccb644
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/dependent/ReceiverService.java
@@ -0,0 +1,91 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.receiver.dependent;
+
+import static com.google.gerrit.k8s.operator.receiver.dependent.ReceiverDeployment.HTTP_PORT;
+
+import com.google.gerrit.k8s.operator.receiver.ReceiverReconciler;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.Receiver;
+import io.fabric8.kubernetes.api.model.Service;
+import io.fabric8.kubernetes.api.model.ServiceBuilder;
+import io.fabric8.kubernetes.api.model.ServicePort;
+import io.fabric8.kubernetes.api.model.ServicePortBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+@KubernetesDependent
+public class ReceiverService extends CRUDKubernetesDependentResource<Service, Receiver> {
+  public static final String HTTP_PORT_NAME = "http";
+
+  public ReceiverService() {
+    super(Service.class);
+  }
+
+  @Override
+  protected Service desired(Receiver receiver, Context<Receiver> context) {
+    return new ServiceBuilder()
+        .withApiVersion("v1")
+        .withNewMetadata()
+        .withName(getName(receiver))
+        .withNamespace(receiver.getMetadata().getNamespace())
+        .withLabels(getLabels(receiver))
+        .endMetadata()
+        .withNewSpec()
+        .withType(receiver.getSpec().getService().getType())
+        .withPorts(getServicePorts(receiver))
+        .withSelector(ReceiverDeployment.getSelectorLabels(receiver))
+        .endSpec()
+        .build();
+  }
+
+  public static String getName(Receiver receiver) {
+    return receiver.getMetadata().getName();
+  }
+
+  public static String getName(String receiverName) {
+    return receiverName;
+  }
+
+  public static Map<String, String> getLabels(Receiver receiver) {
+    return GerritCluster.getLabels(
+        receiver.getMetadata().getName(),
+        "receiver-service",
+        ReceiverReconciler.class.getSimpleName());
+  }
+
+  public static String getHostname(Receiver receiver) {
+    return getHostname(receiver.getMetadata().getName(), receiver.getMetadata().getNamespace());
+  }
+
+  public static String getHostname(String receiverName, String namespace) {
+    return String.format("%s.%s.svc.cluster.local", getName(receiverName), namespace);
+  }
+
+  private static List<ServicePort> getServicePorts(Receiver receiver) {
+    List<ServicePort> ports = new ArrayList<>();
+    ports.add(
+        new ServicePortBuilder()
+            .withName(HTTP_PORT_NAME)
+            .withPort(receiver.getSpec().getService().getHttpPort())
+            .withNewTargetPort(HTTP_PORT)
+            .build());
+    return ports;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/AbstractKeyStoreProvider.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/AbstractKeyStoreProvider.java
new file mode 100644
index 0000000..3b71e89
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/AbstractKeyStoreProvider.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.server;
+
+import java.io.IOException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.util.Base64;
+
+public abstract class AbstractKeyStoreProvider implements KeyStoreProvider {
+  private static final String ALIAS = "operator";
+  private static final String CERT_PREFIX = "-----BEGIN CERTIFICATE-----";
+  private static final String CERT_SUFFIX = "-----END CERTIFICATE-----";
+
+  final String getAlias() {
+    return ALIAS;
+  }
+
+  @Override
+  public final String getCertificate()
+      throws CertificateEncodingException, KeyStoreException, NoSuchAlgorithmException,
+          CertificateException, IOException {
+    StringBuilder cert = new StringBuilder();
+    cert.append(CERT_PREFIX);
+    cert.append("\n");
+    cert.append(
+        Base64.getEncoder().encodeToString(getKeyStore().getCertificate(getAlias()).getEncoded()));
+    cert.append("\n");
+    cert.append(CERT_SUFFIX);
+    return cert.toString();
+  }
+
+  private final KeyStore getKeyStore()
+      throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException {
+    return KeyStore.getInstance(getKeyStorePath().toFile(), getKeyStorePassword().toCharArray());
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/AdmissionWebhookServlet.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/AdmissionWebhookServlet.java
new file mode 100644
index 0000000..a886988
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/AdmissionWebhookServlet.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.server;
+
+import jakarta.servlet.http.HttpServlet;
+
+public abstract class AdmissionWebhookServlet extends HttpServlet {
+  private static final long serialVersionUID = 1L;
+
+  public abstract String getName();
+
+  public abstract String getVersion();
+
+  public abstract String getURI();
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/FileSystemKeyStoreProvider.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/FileSystemKeyStoreProvider.java
new file mode 100644
index 0000000..a56da7f
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/FileSystemKeyStoreProvider.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.server;
+
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+@Singleton
+public class FileSystemKeyStoreProvider extends AbstractKeyStoreProvider {
+  static final String KEYSTORE_PATH = "/operator/keystore.jks";
+  static final String KEYSTORE_PWD_FILE = "/operator/keystore.password";
+
+  @Override
+  public Path getKeyStorePath() {
+    return Path.of(KEYSTORE_PATH);
+  }
+
+  @Override
+  public String getKeyStorePassword() throws IOException {
+    return Files.readString(Path.of(KEYSTORE_PWD_FILE));
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/GeneratedKeyStoreProvider.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/GeneratedKeyStoreProvider.java
new file mode 100644
index 0000000..d96204d
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/GeneratedKeyStoreProvider.java
@@ -0,0 +1,132 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.server;
+
+import static com.google.gerrit.k8s.operator.GerritOperator.SERVICE_NAME;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.nio.file.Path;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.Security;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Date;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.bouncycastle.asn1.ASN1Encodable;
+import org.bouncycastle.asn1.DERSequence;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.GeneralName;
+import org.bouncycastle.cert.CertIOException;
+import org.bouncycastle.cert.X509v3CertificateBuilder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+
+@Singleton
+public class GeneratedKeyStoreProvider extends AbstractKeyStoreProvider {
+  private static final Path KEYSTORE_PATH = Path.of("/tmp/keystore.jks");
+
+  private final String namespace;
+  private final String password;
+
+  @Inject
+  public GeneratedKeyStoreProvider(@Named("Namespace") String namespace) {
+    this.namespace = namespace;
+    this.password = generatePassword();
+    generateKeyStore();
+  }
+
+  @Override
+  public Path getKeyStorePath() {
+    return KEYSTORE_PATH;
+  }
+
+  @Override
+  public String getKeyStorePassword() {
+    return password;
+  }
+
+  private String getCN() {
+    return String.format("%s.%s.svc", SERVICE_NAME, namespace);
+  }
+
+  private String generatePassword() {
+    return RandomStringUtils.randomAlphabetic(10);
+  }
+
+  private Certificate generateCertificate(KeyPair keyPair)
+      throws OperatorCreationException, CertificateException, CertIOException {
+    BouncyCastleProvider bcProvider = new BouncyCastleProvider();
+    Security.addProvider(bcProvider);
+
+    Instant start = Instant.now();
+    X500Name dnName = new X500Name(String.format("cn=%s", getCN()));
+    DERSequence subjectAlternativeNames =
+        new DERSequence(new ASN1Encodable[] {new GeneralName(GeneralName.dNSName, getCN())});
+
+    X509v3CertificateBuilder certBuilder =
+        new JcaX509v3CertificateBuilder(
+                dnName,
+                BigInteger.valueOf(start.toEpochMilli()),
+                Date.from(start),
+                Date.from(start.plus(365, ChronoUnit.DAYS)),
+                dnName,
+                keyPair.getPublic())
+            .addExtension(Extension.subjectAlternativeName, true, subjectAlternativeNames);
+
+    ContentSigner contentSigner =
+        new JcaContentSignerBuilder("SHA256WithRSA").build(keyPair.getPrivate());
+    return new JcaX509CertificateConverter()
+        .setProvider(bcProvider)
+        .getCertificate(certBuilder.build(contentSigner));
+  }
+
+  private void generateKeyStore() {
+    KEYSTORE_PATH.getParent().toFile().mkdirs();
+    try (FileOutputStream fos = new FileOutputStream(KEYSTORE_PATH.toFile())) {
+      KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
+      keyPairGenerator.initialize(4096);
+      KeyPair keyPair = keyPairGenerator.generateKeyPair();
+
+      Certificate[] chain = {generateCertificate(keyPair)};
+
+      KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+      keyStore.load(null, null);
+      keyStore.setKeyEntry(getAlias(), keyPair.getPrivate(), password.toCharArray(), chain);
+      keyStore.store(fos, password.toCharArray());
+    } catch (IOException
+        | NoSuchAlgorithmException
+        | CertificateException
+        | KeyStoreException
+        | OperatorCreationException e) {
+      throw new IllegalStateException("Failed to create keystore.", e);
+    }
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/HealthcheckServlet.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/HealthcheckServlet.java
new file mode 100644
index 0000000..6097bde
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/HealthcheckServlet.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.server;
+
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+public class HealthcheckServlet extends HttpServlet {
+  private static final long serialVersionUID = 1L;
+
+  protected void doGet(HttpServletRequest request, HttpServletResponse response)
+      throws IOException {
+    response.setContentType("application/text");
+    response.setStatus(HttpServletResponse.SC_OK);
+    response.getWriter().println("ALL GOOD.");
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/HttpServer.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/HttpServer.java
new file mode 100644
index 0000000..23b8055
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/HttpServer.java
@@ -0,0 +1,72 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.server;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.Set;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.SecureRequestCustomizer;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.servlet.ServletHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+
+@Singleton
+public class HttpServer {
+  public static final String KEYSTORE_PATH = "/operator/keystore.jks";
+  public static final String KEYSTORE_PWD_FILE = "/operator/keystore.password";
+  public static final int PORT = 8080;
+
+  private final Server server = new Server();
+  private final KeyStoreProvider keyStoreProvider;
+  private final Set<AdmissionWebhookServlet> admissionWebhookServlets;
+
+  @Inject
+  public HttpServer(
+      KeyStoreProvider keyStoreProvider, Set<AdmissionWebhookServlet> admissionWebhookServlets) {
+    this.keyStoreProvider = keyStoreProvider;
+    this.admissionWebhookServlets = admissionWebhookServlets;
+  }
+
+  public void start() throws Exception {
+    SslContextFactory.Server ssl = new SslContextFactory.Server();
+    ssl.setKeyStorePath(keyStoreProvider.getKeyStorePath().toString());
+    ssl.setTrustStorePath(keyStoreProvider.getKeyStorePath().toString());
+    ssl.setKeyStorePassword(keyStoreProvider.getKeyStorePassword());
+    ssl.setTrustStorePassword(keyStoreProvider.getKeyStorePassword());
+    ssl.setSniRequired(false);
+
+    HttpConfiguration sslConfiguration = new HttpConfiguration();
+    sslConfiguration.addCustomizer(new SecureRequestCustomizer(false));
+    HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory(sslConfiguration);
+
+    ServerConnector connector = new ServerConnector(server, ssl, httpConnectionFactory);
+    connector.setPort(PORT);
+    server.setConnectors(new Connector[] {connector});
+
+    ServletHandler servletHandler = new ServletHandler();
+    for (AdmissionWebhookServlet servlet : admissionWebhookServlets) {
+      servletHandler.addServletWithMapping(new ServletHolder(servlet), servlet.getURI());
+    }
+    servletHandler.addServletWithMapping(HealthcheckServlet.class, "/health");
+    server.setHandler(servletHandler);
+
+    server.start();
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/KeyStoreProvider.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/KeyStoreProvider.java
new file mode 100644
index 0000000..c41777f
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/KeyStoreProvider.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.server;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+
+public interface KeyStoreProvider {
+  Path getKeyStorePath();
+
+  String getKeyStorePassword() throws IOException;
+
+  String getCertificate()
+      throws CertificateEncodingException, KeyStoreException, NoSuchAlgorithmException,
+          CertificateException, IOException;
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/ServerModule.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/ServerModule.java
new file mode 100644
index 0000000..ae613dd
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/ServerModule.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.server;
+
+import static com.google.gerrit.k8s.operator.server.FileSystemKeyStoreProvider.KEYSTORE_PATH;
+
+import com.google.gerrit.k8s.operator.v1alpha.admission.servlet.GerritAdmissionWebhook;
+import com.google.gerrit.k8s.operator.v1alpha.admission.servlet.GerritClusterAdmissionWebhook;
+import com.google.gerrit.k8s.operator.v1alpha.admission.servlet.GitGcAdmissionWebhook;
+import com.google.inject.AbstractModule;
+import com.google.inject.multibindings.Multibinder;
+import java.io.File;
+
+public class ServerModule extends AbstractModule {
+  public void configure() {
+    if (new File(KEYSTORE_PATH).exists()) {
+      bind(KeyStoreProvider.class).to(FileSystemKeyStoreProvider.class);
+    } else {
+      bind(KeyStoreProvider.class).to(GeneratedKeyStoreProvider.class);
+    }
+    bind(HttpServer.class);
+    Multibinder<AdmissionWebhookServlet> admissionWebhookServlets =
+        Multibinder.newSetBinder(binder(), AdmissionWebhookServlet.class);
+    admissionWebhookServlets.addBinding().to(GerritClusterAdmissionWebhook.class);
+    admissionWebhookServlets.addBinding().to(GitGcAdmissionWebhook.class);
+    admissionWebhookServlets.addBinding().to(GerritAdmissionWebhook.class);
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/ValidatingAdmissionWebhookServlet.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/ValidatingAdmissionWebhookServlet.java
new file mode 100644
index 0000000..f6d7c39
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/ValidatingAdmissionWebhookServlet.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.server;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.flogger.FluentLogger;
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.fabric8.kubernetes.api.model.Status;
+import io.fabric8.kubernetes.api.model.admission.v1.AdmissionResponseBuilder;
+import io.fabric8.kubernetes.api.model.admission.v1.AdmissionReview;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+public abstract class ValidatingAdmissionWebhookServlet extends AdmissionWebhookServlet {
+  private static final long serialVersionUID = 1L;
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  public abstract Status validate(HasMetadata resource);
+
+  @Override
+  public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
+    ObjectMapper objectMapper = new ObjectMapper();
+    AdmissionReview admissionReq =
+        objectMapper.readValue(request.getInputStream(), AdmissionReview.class);
+
+    logger.atFine().log("Admission request received: %s", admissionReq.toString());
+
+    response.setContentType("application/json");
+    AdmissionResponseBuilder admissionRespBuilder =
+        new AdmissionResponseBuilder().withUid(admissionReq.getRequest().getUid());
+    Status validationStatus = validate((HasMetadata) admissionReq.getRequest().getObject());
+    response.setStatus(HttpServletResponse.SC_OK);
+    if (validationStatus.getCode() < 400) {
+      admissionRespBuilder = admissionRespBuilder.withAllowed(true);
+    } else {
+      admissionRespBuilder = admissionRespBuilder.withAllowed(false).withStatus(validationStatus);
+    }
+    admissionReq.setResponse(admissionRespBuilder.build());
+    objectMapper.writeValue(response.getWriter(), admissionReq);
+    logger.atFine().log(
+        "Admission request responded with %s", admissionReq.getResponse().toString());
+  }
+
+  @Override
+  public String getURI() {
+    return String.format("/admission/%s/%s", getVersion(), getName());
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/util/CRUDKubernetesDependentPVCResource.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/util/CRUDKubernetesDependentPVCResource.java
new file mode 100644
index 0000000..cd5bd8e
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/util/CRUDKubernetesDependentPVCResource.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.util;
+
+import com.google.common.flogger.FluentLogger;
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.fabric8.kubernetes.api.model.PersistentVolumeClaim;
+import io.fabric8.kubernetes.api.model.PersistentVolumeClaimSpec;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
+
+public abstract class CRUDKubernetesDependentPVCResource<P extends HasMetadata>
+    extends CRUDKubernetesDependentResource<PersistentVolumeClaim, P> {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  public CRUDKubernetesDependentPVCResource() {
+    super(PersistentVolumeClaim.class);
+  }
+
+  @Override
+  protected final PersistentVolumeClaim desired(P primary, Context<P> context) {
+    PersistentVolumeClaim pvc = desiredPVC(primary, context);
+    PersistentVolumeClaim existingPvc =
+        client
+            .persistentVolumeClaims()
+            .inNamespace(pvc.getMetadata().getNamespace())
+            .withName(pvc.getMetadata().getName())
+            .get();
+    String volumeName = pvc.getSpec().getVolumeName();
+    if (existingPvc != null && (volumeName == null || volumeName.isEmpty())) {
+      logger.atFine().log(
+          "PVC %s/%s already has bound a PV. Keeping volumeName reference.",
+          pvc.getMetadata().getNamespace(), pvc.getMetadata().getName());
+      PersistentVolumeClaimSpec pvcSpec = pvc.getSpec();
+      pvcSpec.setVolumeName(existingPvc.getSpec().getVolumeName());
+      pvc.setSpec(pvcSpec);
+    }
+    return pvc;
+  }
+
+  protected abstract PersistentVolumeClaim desiredPVC(P primary, Context<P> context);
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/admission/servlet/GerritAdmissionWebhook.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/admission/servlet/GerritAdmissionWebhook.java
new file mode 100644
index 0000000..f74cb59
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/admission/servlet/GerritAdmissionWebhook.java
@@ -0,0 +1,116 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.admission.servlet;
+
+import static com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GlobalRefDbConfig.RefDatabase.SPANNER;
+import static com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GlobalRefDbConfig.RefDatabase.ZOOKEEPER;
+
+import com.google.gerrit.k8s.operator.gerrit.config.InvalidGerritConfigException;
+import com.google.gerrit.k8s.operator.server.ValidatingAdmissionWebhookServlet;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GlobalRefDbConfig;
+import com.google.gerrit.k8s.operator.v1alpha.gerrit.config.GerritConfigBuilder;
+import com.google.inject.Singleton;
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.fabric8.kubernetes.api.model.Status;
+import io.fabric8.kubernetes.api.model.StatusBuilder;
+import jakarta.servlet.http.HttpServletResponse;
+import java.util.Locale;
+
+@Singleton
+public class GerritAdmissionWebhook extends ValidatingAdmissionWebhookServlet {
+  private static final long serialVersionUID = 1L;
+
+  @Override
+  public Status validate(HasMetadata resource) {
+    if (!(resource instanceof Gerrit)) {
+      return new StatusBuilder()
+          .withCode(HttpServletResponse.SC_BAD_REQUEST)
+          .withMessage("Invalid resource. Expected Gerrit-resource for validation.")
+          .build();
+    }
+
+    Gerrit gerrit = (Gerrit) resource;
+
+    try {
+      invalidGerritConfiguration(gerrit);
+    } catch (InvalidGerritConfigException e) {
+      return new StatusBuilder()
+          .withCode(HttpServletResponse.SC_BAD_REQUEST)
+          .withMessage(e.getMessage())
+          .build();
+    }
+
+    if (noRefDbConfiguredForHA(gerrit)) {
+      return new StatusBuilder()
+          .withCode(HttpServletResponse.SC_BAD_REQUEST)
+          .withMessage(
+              "A Ref-Database is required to horizontally scale a primary Gerrit: .spec.refdb.database != NONE")
+          .build();
+    }
+
+    if (missingRefdbConfig(gerrit)) {
+      String refDbName = "";
+      switch (gerrit.getSpec().getRefdb().getDatabase()) {
+        case ZOOKEEPER:
+          refDbName = ZOOKEEPER.toString().toLowerCase(Locale.US);
+          break;
+        case SPANNER:
+          refDbName = SPANNER.toString().toLowerCase(Locale.US);
+          break;
+        default:
+          break;
+      }
+      return new StatusBuilder()
+          .withCode(HttpServletResponse.SC_BAD_REQUEST)
+          .withMessage(
+              String.format("Missing %s configuration (.spec.refdb.%s)", refDbName, refDbName))
+          .build();
+    }
+
+    return new StatusBuilder().withCode(HttpServletResponse.SC_OK).build();
+  }
+
+  private void invalidGerritConfiguration(Gerrit gerrit) throws InvalidGerritConfigException {
+    new GerritConfigBuilder(gerrit).validate();
+  }
+
+  private boolean noRefDbConfiguredForHA(Gerrit gerrit) {
+    return gerrit.getSpec().isHighlyAvailablePrimary()
+        && gerrit.getSpec().getRefdb().getDatabase().equals(GlobalRefDbConfig.RefDatabase.NONE);
+  }
+
+  private boolean missingRefdbConfig(Gerrit gerrit) {
+    GlobalRefDbConfig refDbConfig = gerrit.getSpec().getRefdb();
+    switch (refDbConfig.getDatabase()) {
+      case ZOOKEEPER:
+        return refDbConfig.getZookeeper() == null;
+      case SPANNER:
+        return refDbConfig.getSpanner() == null;
+      default:
+        return false;
+    }
+  }
+
+  @Override
+  public String getName() {
+    return "gerrit";
+  }
+
+  @Override
+  public String getVersion() {
+    return "v1alpha";
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/admission/servlet/GerritClusterAdmissionWebhook.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/admission/servlet/GerritClusterAdmissionWebhook.java
new file mode 100644
index 0000000..8817559
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/admission/servlet/GerritClusterAdmissionWebhook.java
@@ -0,0 +1,103 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.admission.servlet;
+
+import com.google.gerrit.k8s.operator.server.ValidatingAdmissionWebhookServlet;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplate;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplateSpec.GerritMode;
+import com.google.inject.Singleton;
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.fabric8.kubernetes.api.model.Status;
+import io.fabric8.kubernetes.api.model.StatusBuilder;
+import jakarta.servlet.http.HttpServletResponse;
+
+@Singleton
+public class GerritClusterAdmissionWebhook extends ValidatingAdmissionWebhookServlet {
+  private static final long serialVersionUID = 1L;
+
+  @Override
+  public Status validate(HasMetadata resource) {
+    if (!(resource instanceof GerritCluster)) {
+      return new StatusBuilder()
+          .withCode(HttpServletResponse.SC_BAD_REQUEST)
+          .withMessage("Invalid resource. Expected GerritCluster-resource for validation.")
+          .build();
+    }
+
+    GerritCluster gerritCluster = (GerritCluster) resource;
+
+    if (multiplePrimaryGerritInCluster(gerritCluster)) {
+      return new StatusBuilder()
+          .withCode(HttpServletResponse.SC_CONFLICT)
+          .withMessage("Only a single primary Gerrit is allowed per Gerrit Cluster.")
+          .build();
+    }
+
+    if (primaryGerritAndReceiverInCluster(gerritCluster)) {
+      return new StatusBuilder()
+          .withCode(HttpServletResponse.SC_CONFLICT)
+          .withMessage("A primary Gerrit cannot be in the same Gerrit Cluster as a Receiver.")
+          .build();
+    }
+
+    if (multipleGerritReplicaInCluster(gerritCluster)) {
+      return new StatusBuilder()
+          .withCode(HttpServletResponse.SC_CONFLICT)
+          .withMessage("Only a single Gerrit Replica is allowed per Gerrit Cluster.")
+          .build();
+    }
+
+    GerritAdmissionWebhook gerritAdmission = new GerritAdmissionWebhook();
+    for (GerritTemplate gerrit : gerritCluster.getSpec().getGerrits()) {
+      Status status = gerritAdmission.validate(gerrit.toGerrit(gerritCluster));
+      if (status.getCode() != HttpServletResponse.SC_OK) {
+        return status;
+      }
+    }
+
+    return new StatusBuilder().withCode(HttpServletResponse.SC_OK).build();
+  }
+
+  private boolean multiplePrimaryGerritInCluster(GerritCluster gerritCluster) {
+    return gerritCluster.getSpec().getGerrits().stream()
+            .filter(g -> g.getSpec().getMode() == GerritMode.PRIMARY)
+            .count()
+        > 1;
+  }
+
+  private boolean primaryGerritAndReceiverInCluster(GerritCluster gerritCluster) {
+    return gerritCluster.getSpec().getGerrits().stream()
+            .anyMatch(g -> g.getSpec().getMode() == GerritMode.PRIMARY)
+        && gerritCluster.getSpec().getReceiver() != null;
+  }
+
+  private boolean multipleGerritReplicaInCluster(GerritCluster gerritCluster) {
+    return gerritCluster.getSpec().getGerrits().stream()
+            .filter(g -> g.getSpec().getMode() == GerritMode.REPLICA)
+            .count()
+        > 1;
+  }
+
+  @Override
+  public String getName() {
+    return "gerritcluster";
+  }
+
+  @Override
+  public String getVersion() {
+    return "v1alpha";
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/admission/servlet/GitGcAdmissionWebhook.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/admission/servlet/GitGcAdmissionWebhook.java
new file mode 100644
index 0000000..3198c1a
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/admission/servlet/GitGcAdmissionWebhook.java
@@ -0,0 +1,114 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.admission.servlet;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.k8s.operator.server.ValidatingAdmissionWebhookServlet;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gitgc.GitGarbageCollection;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.fabric8.kubernetes.api.model.Status;
+import io.fabric8.kubernetes.api.model.StatusBuilder;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import jakarta.servlet.http.HttpServletResponse;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Singleton
+public class GitGcAdmissionWebhook extends ValidatingAdmissionWebhookServlet {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  private static final long serialVersionUID = 1L;
+  private static final Status OK_STATUS =
+      new StatusBuilder().withCode(HttpServletResponse.SC_OK).build();
+
+  private final KubernetesClient client;
+
+  @Inject
+  public GitGcAdmissionWebhook(KubernetesClient client) {
+    this.client = client;
+  }
+
+  @Override
+  public Status validate(HasMetadata resource) {
+    if (!(resource instanceof GitGarbageCollection)) {
+      return new StatusBuilder()
+          .withCode(HttpServletResponse.SC_BAD_REQUEST)
+          .withMessage("Invalid resource. Expected GitGarbageCollection-resource for validation.")
+          .build();
+    }
+
+    GitGarbageCollection gitGc = (GitGarbageCollection) resource;
+
+    String gitGcUid = gitGc.getMetadata().getUid();
+    List<GitGarbageCollection> gitGcs =
+        client
+            .resources(GitGarbageCollection.class)
+            .inNamespace(gitGc.getMetadata().getNamespace())
+            .list()
+            .getItems()
+            .stream()
+            .filter(gc -> !gc.getMetadata().getUid().equals(gitGcUid))
+            .collect(Collectors.toList());
+    Set<String> projects = gitGc.getSpec().getProjects();
+
+    logger.atFine().log("Detected GitGcs: %s", gitGcs);
+    if (projects.isEmpty()) {
+      if (gitGcs.stream().anyMatch(gc -> gc.getSpec().getProjects().isEmpty())) {
+        return new StatusBuilder()
+            .withCode(HttpServletResponse.SC_CONFLICT)
+            .withMessage("Only a single GitGc working on all projects allowed per GerritCluster.")
+            .build();
+      }
+      return OK_STATUS;
+    }
+
+    Set<String> projectsWithExistingGC =
+        gitGcs.stream()
+            .map(gc -> gc.getSpec().getProjects())
+            .flatMap(Collection::stream)
+            .collect(Collectors.toSet());
+    Set<String> projectsIntersection = getIntersection(projects, projectsWithExistingGC);
+    if (projectsIntersection.isEmpty()) {
+      return OK_STATUS;
+    }
+    return new StatusBuilder()
+        .withCode(HttpServletResponse.SC_CONFLICT)
+        .withMessage(
+            "Only a single GitGc is allowed to work on a given project. Conflict for projects: "
+                + projectsIntersection)
+        .build();
+  }
+
+  private Set<String> getIntersection(Set<String> set1, Set<String> set2) {
+    Set<String> intersection = new HashSet<>();
+    intersection.addAll(set1);
+    intersection.retainAll(set2);
+    return intersection;
+  }
+
+  @Override
+  public String getName() {
+    return "gitgc";
+  }
+
+  @Override
+  public String getVersion() {
+    return "v1alpha";
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/cluster/GerritCluster.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/cluster/GerritCluster.java
new file mode 100644
index 0000000..b88ab09
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/cluster/GerritCluster.java
@@ -0,0 +1,242 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.cluster;
+
+import static com.google.gerrit.k8s.operator.cluster.dependent.NfsIdmapdConfigMap.NFS_IDMAPD_CM_NAME;
+import static com.google.gerrit.k8s.operator.cluster.dependent.SharedPVC.SHARED_PVC_NAME;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.ContainerImageConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.SharedStorage.ExternalPVCConfig;
+import io.fabric8.kubernetes.api.model.Container;
+import io.fabric8.kubernetes.api.model.ContainerBuilder;
+import io.fabric8.kubernetes.api.model.EnvVar;
+import io.fabric8.kubernetes.api.model.EnvVarBuilder;
+import io.fabric8.kubernetes.api.model.Namespaced;
+import io.fabric8.kubernetes.api.model.Volume;
+import io.fabric8.kubernetes.api.model.VolumeBuilder;
+import io.fabric8.kubernetes.api.model.VolumeMount;
+import io.fabric8.kubernetes.api.model.VolumeMountBuilder;
+import io.fabric8.kubernetes.client.CustomResource;
+import io.fabric8.kubernetes.model.annotation.Group;
+import io.fabric8.kubernetes.model.annotation.ShortNames;
+import io.fabric8.kubernetes.model.annotation.Version;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+@Group("gerritoperator.google.com")
+@Version("v1alpha17")
+@ShortNames("gclus")
+public class GerritCluster extends CustomResource<GerritClusterSpec, GerritClusterStatus>
+    implements Namespaced {
+  private static final long serialVersionUID = 2L;
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  private static final String SHARED_VOLUME_NAME = "shared";
+  private static final String NFS_IDMAPD_CONFIG_VOLUME_NAME = "nfs-config";
+  private static final int GERRIT_FS_UID = 1000;
+  private static final int GERRIT_FS_GID = 100;
+  public static final String PLUGIN_CACHE_MOUNT_PATH = "/var/mnt/plugin_cache";
+  public static final String PLUGIN_CACHE_SUB_DIR = "plugin_cache";
+
+  public String toString() {
+    return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE);
+  }
+
+  @JsonIgnore
+  public Map<String, String> getLabels(String component, String createdBy) {
+    return getLabels(getMetadata().getName(), component, createdBy);
+  }
+
+  // TODO(Thomas): Having so many string parameters is bad. The only parameter should be the
+  // Kubernetes resource that implements an interface that provides methods to retrieve the
+  // required information.
+  @JsonIgnore
+  public static Map<String, String> getLabels(String instance, String component, String createdBy) {
+    Map<String, String> labels = new HashMap<>();
+
+    labels.putAll(getSelectorLabels(instance, component));
+    String version = GerritCluster.class.getPackage().getImplementationVersion();
+    if (version == null || version.isBlank()) {
+      logger.atWarning().log("Unable to read Gerrit Operator version from jar.");
+      version = "unknown";
+    }
+    labels.put("app.kubernetes.io/version", version);
+    labels.put("app.kubernetes.io/created-by", createdBy);
+
+    return labels;
+  }
+
+  @JsonIgnore
+  public static Map<String, String> getSelectorLabels(String instance, String component) {
+    Map<String, String> labels = new HashMap<>();
+
+    labels.put("app.kubernetes.io/name", "gerrit");
+    labels.put("app.kubernetes.io/instance", instance);
+    labels.put("app.kubernetes.io/component", component);
+    labels.put("app.kubernetes.io/part-of", instance);
+    labels.put("app.kubernetes.io/managed-by", "gerrit-operator");
+
+    return labels;
+  }
+
+  @JsonIgnore
+  public static Volume getSharedVolume(ExternalPVCConfig externalPVC) {
+    String claimName = externalPVC.isEnabled() ? externalPVC.getClaimName() : SHARED_PVC_NAME;
+    return new VolumeBuilder()
+        .withName(SHARED_VOLUME_NAME)
+        .withNewPersistentVolumeClaim()
+        .withClaimName(claimName)
+        .endPersistentVolumeClaim()
+        .build();
+  }
+
+  @JsonIgnore
+  public static VolumeMount getGitRepositoriesVolumeMount() {
+    return getGitRepositoriesVolumeMount("/var/mnt/git");
+  }
+
+  @JsonIgnore
+  public static VolumeMount getGitRepositoriesVolumeMount(String mountPath) {
+    return new VolumeMountBuilder()
+        .withName(SHARED_VOLUME_NAME)
+        .withSubPath("git")
+        .withMountPath(mountPath)
+        .build();
+  }
+
+  @JsonIgnore
+  public static VolumeMount getHAShareVolumeMount() {
+    return getSharedVolumeMount("shared", "/var/mnt/shared");
+  }
+
+  @JsonIgnore
+  public static VolumeMount getPluginCacheVolumeMount() {
+    return getSharedVolumeMount(PLUGIN_CACHE_SUB_DIR, "/var/mnt/plugin_cache");
+  }
+
+  @JsonIgnore
+  public static VolumeMount getSharedVolumeMount(String subPath, String mountPath) {
+    return new VolumeMountBuilder()
+        .withName(SHARED_VOLUME_NAME)
+        .withSubPath(subPath)
+        .withMountPath(mountPath)
+        .build();
+  }
+
+  @JsonIgnore
+  public static VolumeMount getLogsVolumeMount() {
+    return getLogsVolumeMount("/var/mnt/logs");
+  }
+
+  @JsonIgnore
+  public static VolumeMount getLogsVolumeMount(String mountPath) {
+    return new VolumeMountBuilder()
+        .withName(SHARED_VOLUME_NAME)
+        .withSubPathExpr("logs/$(POD_NAME)")
+        .withMountPath(mountPath)
+        .build();
+  }
+
+  @JsonIgnore
+  public static Volume getNfsImapdConfigVolume() {
+    return new VolumeBuilder()
+        .withName(NFS_IDMAPD_CONFIG_VOLUME_NAME)
+        .withNewConfigMap()
+        .withName(NFS_IDMAPD_CM_NAME)
+        .endConfigMap()
+        .build();
+  }
+
+  @JsonIgnore
+  public static VolumeMount getNfsImapdConfigVolumeMount() {
+    return new VolumeMountBuilder()
+        .withName(NFS_IDMAPD_CONFIG_VOLUME_NAME)
+        .withMountPath("/etc/idmapd.conf")
+        .withSubPath("idmapd.conf")
+        .build();
+  }
+
+  @JsonIgnore
+  public Container createNfsInitContainer() {
+    return createNfsInitContainer(
+        getSpec().getStorage().getStorageClasses().getNfsWorkaround().getIdmapdConfig() != null,
+        getSpec().getContainerImages());
+  }
+
+  @JsonIgnore
+  public static Container createNfsInitContainer(
+      boolean configureIdmapd, ContainerImageConfig imageConfig) {
+    return createNfsInitContainer(configureIdmapd, imageConfig, List.of());
+  }
+
+  @JsonIgnore
+  public static Container createNfsInitContainer(
+      boolean configureIdmapd,
+      ContainerImageConfig imageConfig,
+      List<VolumeMount> additionalVolumeMounts) {
+    List<VolumeMount> volumeMounts = new ArrayList<>();
+    volumeMounts.add(getLogsVolumeMount());
+    volumeMounts.add(getGitRepositoriesVolumeMount());
+
+    volumeMounts.addAll(additionalVolumeMounts);
+
+    if (configureIdmapd) {
+      volumeMounts.add(getNfsImapdConfigVolumeMount());
+    }
+
+    StringBuilder args = new StringBuilder();
+    args.append("chown -R ");
+    args.append(GERRIT_FS_UID);
+    args.append(":");
+    args.append(GERRIT_FS_GID);
+    args.append(" ");
+    for (VolumeMount vm : volumeMounts) {
+      args.append(vm.getMountPath());
+      args.append(" ");
+    }
+
+    return new ContainerBuilder()
+        .withName("nfs-init")
+        .withImagePullPolicy(imageConfig.getImagePullPolicy())
+        .withImage(imageConfig.getBusyBox().getBusyBoxImage())
+        .withCommand(List.of("sh", "-c"))
+        .withArgs(args.toString().trim())
+        .withEnv(getPodNameEnvVar())
+        .withVolumeMounts(volumeMounts)
+        .build();
+  }
+
+  @JsonIgnore
+  public static EnvVar getPodNameEnvVar() {
+    return new EnvVarBuilder()
+        .withName("POD_NAME")
+        .withNewValueFrom()
+        .withNewFieldRef()
+        .withFieldPath("metadata.name")
+        .endFieldRef()
+        .endValueFrom()
+        .build();
+  }
+
+  @JsonIgnore
+  public String getDependentResourceName(String nameSuffix) {
+    return String.format("%s-%s", getMetadata().getName(), nameSuffix);
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/cluster/GerritClusterSpec.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/cluster/GerritClusterSpec.java
new file mode 100644
index 0000000..956c678
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/cluster/GerritClusterSpec.java
@@ -0,0 +1,91 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.cluster;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplate;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.ReceiverTemplate;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.ContainerImageConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GerritClusterIngressConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GerritStorageConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GlobalRefDbConfig;
+import java.util.ArrayList;
+import java.util.List;
+
+public class GerritClusterSpec {
+
+  private GerritStorageConfig storage = new GerritStorageConfig();
+  private ContainerImageConfig containerImages = new ContainerImageConfig();
+  private GerritClusterIngressConfig ingress = new GerritClusterIngressConfig();
+  private GlobalRefDbConfig refdb = new GlobalRefDbConfig();
+  private String serverId = "";
+  private List<GerritTemplate> gerrits = new ArrayList<>();
+  private ReceiverTemplate receiver;
+
+  public GerritStorageConfig getStorage() {
+    return storage;
+  }
+
+  public void setStorage(GerritStorageConfig storage) {
+    this.storage = storage;
+  }
+
+  public ContainerImageConfig getContainerImages() {
+    return containerImages;
+  }
+
+  public void setContainerImages(ContainerImageConfig containerImages) {
+    this.containerImages = containerImages;
+  }
+
+  public GerritClusterIngressConfig getIngress() {
+    return ingress;
+  }
+
+  public void setIngress(GerritClusterIngressConfig ingress) {
+    this.ingress = ingress;
+  }
+
+  public GlobalRefDbConfig getRefdb() {
+    return refdb;
+  }
+
+  public void setRefdb(GlobalRefDbConfig refdb) {
+    this.refdb = refdb;
+  }
+
+  public String getServerId() {
+    return serverId;
+  }
+
+  public void setServerId(String serverId) {
+    this.serverId = serverId;
+  }
+
+  public List<GerritTemplate> getGerrits() {
+    return gerrits;
+  }
+
+  public void setGerrits(List<GerritTemplate> gerrits) {
+    this.gerrits = gerrits;
+  }
+
+  public ReceiverTemplate getReceiver() {
+    return receiver;
+  }
+
+  public void setReceiver(ReceiverTemplate receiver) {
+    this.receiver = receiver;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/cluster/GerritClusterStatus.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/cluster/GerritClusterStatus.java
new file mode 100644
index 0000000..a0b853d
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/cluster/GerritClusterStatus.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.cluster;
+
+import java.util.List;
+import java.util.Map;
+
+public class GerritClusterStatus {
+  private Map<String, List<String>> members;
+
+  public Map<String, List<String>> getMembers() {
+    return members;
+  }
+
+  public void setMembers(Map<String, List<String>> members) {
+    this.members = members;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/Gerrit.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/Gerrit.java
new file mode 100644
index 0000000..4027cdd
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/Gerrit.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import io.fabric8.kubernetes.api.model.Namespaced;
+import io.fabric8.kubernetes.client.CustomResource;
+import io.fabric8.kubernetes.model.annotation.Group;
+import io.fabric8.kubernetes.model.annotation.ShortNames;
+import io.fabric8.kubernetes.model.annotation.Version;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+@Group("gerritoperator.google.com")
+@Version("v1alpha17")
+@ShortNames("gcr")
+public class Gerrit extends CustomResource<GerritSpec, GerritStatus> implements Namespaced {
+  private static final long serialVersionUID = 2L;
+
+  public String toString() {
+    return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE);
+  }
+
+  @JsonIgnore
+  public boolean isSshEnabled() {
+    return getSpec().getService().getSshPort() > 0;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritDebugConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritDebugConfig.java
new file mode 100644
index 0000000..7e7a5fd
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritDebugConfig.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit;
+
+public class GerritDebugConfig {
+  private boolean enabled;
+  private boolean suspend;
+
+  public boolean isEnabled() {
+    return enabled;
+  }
+
+  public void setEnabled(boolean enabled) {
+    this.enabled = enabled;
+  }
+
+  public boolean isSuspend() {
+    return suspend;
+  }
+
+  public void setSuspend(boolean suspend) {
+    this.suspend = suspend;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritInitConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritInitConfig.java
new file mode 100644
index 0000000..82dc41f
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritInitConfig.java
@@ -0,0 +1,89 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
+
+public class GerritInitConfig {
+  private String caCertPath = "/var/config/ca.crt";
+  private boolean pluginCacheEnabled;
+  private String pluginCacheDir = "/var/mnt/plugins";
+  private List<GerritPlugin> plugins;
+  private List<GerritModule> libs;
+
+  @JsonProperty("highAvailability")
+  private boolean isHighlyAvailable;
+
+  private String refdb;
+
+  public String getCaCertPath() {
+    return caCertPath;
+  }
+
+  public void setCaCertPath(String caCertPath) {
+    this.caCertPath = caCertPath;
+  }
+
+  public boolean isPluginCacheEnabled() {
+    return pluginCacheEnabled;
+  }
+
+  public void setPluginCacheEnabled(boolean pluginCacheEnabled) {
+    this.pluginCacheEnabled = pluginCacheEnabled;
+  }
+
+  public String getPluginCacheDir() {
+    return pluginCacheDir;
+  }
+
+  public void setPluginCacheDir(String pluginCacheDir) {
+    this.pluginCacheDir = pluginCacheDir;
+  }
+
+  public List<GerritPlugin> getPlugins() {
+    return plugins;
+  }
+
+  public void setPlugins(List<GerritPlugin> plugins) {
+    this.plugins = plugins;
+  }
+
+  public List<GerritModule> getLibs() {
+    return libs;
+  }
+
+  public void setLibs(List<GerritModule> libs) {
+    this.libs = libs;
+  }
+
+  @JsonProperty("highAvailability")
+  public boolean isHighlyAvailable() {
+    return isHighlyAvailable;
+  }
+
+  @JsonProperty("highAvailability")
+  public void setHighlyAvailable(boolean isHighlyAvailable) {
+    this.isHighlyAvailable = isHighlyAvailable;
+  }
+
+  public String getRefdb() {
+    return refdb;
+  }
+
+  public void setRefdb(String refdb) {
+    this.refdb = refdb;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritModule.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritModule.java
new file mode 100644
index 0000000..5b0f241
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritModule.java
@@ -0,0 +1,66 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import java.io.Serializable;
+
+public class GerritModule implements Serializable {
+  private static final long serialVersionUID = 1L;
+
+  private String name;
+
+  @JsonInclude(JsonInclude.Include.NON_EMPTY)
+  private String url;
+
+  @JsonInclude(JsonInclude.Include.NON_EMPTY)
+  private String sha1;
+
+  public GerritModule() {}
+
+  public GerritModule(String name) {
+    this.name = name;
+  }
+
+  public GerritModule(String name, String url, String sha1) {
+    this.name = name;
+    this.url = url;
+    this.sha1 = sha1;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public void setName(String name) {
+    this.name = name;
+  }
+
+  public String getUrl() {
+    return url;
+  }
+
+  public void setUrl(String url) {
+    this.url = url;
+  }
+
+  public String getSha1() {
+    return sha1;
+  }
+
+  public void setSha1(String sha1) {
+    this.sha1 = sha1;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritPlugin.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritPlugin.java
new file mode 100644
index 0000000..196ecb4
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritPlugin.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+public class GerritPlugin extends GerritModule {
+  private static final long serialVersionUID = 1L;
+
+  @JsonInclude(JsonInclude.Include.NON_EMPTY)
+  private boolean installAsLibrary = false;
+
+  public GerritPlugin() {}
+
+  public GerritPlugin(String name) {
+    super(name);
+  }
+
+  public GerritPlugin(String name, String url, String sha1) {
+    super(name, url, sha1);
+  }
+
+  public boolean isInstallAsLibrary() {
+    return installAsLibrary;
+  }
+
+  public void setInstallAsLibrary(boolean installAsLibrary) {
+    this.installAsLibrary = installAsLibrary;
+  }
+
+  @JsonIgnore
+  public boolean isPackagedPlugin() {
+    return getUrl() == null;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritProbe.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritProbe.java
new file mode 100644
index 0000000..46733ed
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritProbe.java
@@ -0,0 +1,81 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.google.gerrit.k8s.operator.gerrit.dependent.GerritStatefulSet;
+import io.fabric8.kubernetes.api.model.ExecAction;
+import io.fabric8.kubernetes.api.model.GRPCAction;
+import io.fabric8.kubernetes.api.model.HTTPGetAction;
+import io.fabric8.kubernetes.api.model.HTTPGetActionBuilder;
+import io.fabric8.kubernetes.api.model.IntOrString;
+import io.fabric8.kubernetes.api.model.Probe;
+import io.fabric8.kubernetes.api.model.TCPSocketAction;
+
+public class GerritProbe extends Probe {
+  private static final long serialVersionUID = 1L;
+
+  private static final HTTPGetAction HTTP_GET_ACTION =
+      new HTTPGetActionBuilder()
+          .withPath("/config/server/healthcheck~status")
+          .withPort(new IntOrString(GerritStatefulSet.HTTP_PORT))
+          .build();
+
+  @JsonIgnore private ExecAction exec;
+
+  @JsonIgnore private GRPCAction grpc;
+
+  @JsonIgnore private TCPSocketAction tcpSocket;
+
+  @Override
+  public void setExec(ExecAction exec) {
+    super.setExec(null);
+  }
+
+  @Override
+  public void setGrpc(GRPCAction grpc) {
+    super.setGrpc(null);
+  }
+
+  @Override
+  public void setHttpGet(HTTPGetAction httpGet) {
+    super.setHttpGet(HTTP_GET_ACTION);
+  }
+
+  @Override
+  public void setTcpSocket(TCPSocketAction tcpSocket) {
+    super.setTcpSocket(null);
+  }
+
+  @Override
+  public ExecAction getExec() {
+    return null;
+  }
+
+  @Override
+  public GRPCAction getGrpc() {
+    return null;
+  }
+
+  @Override
+  public HTTPGetAction getHttpGet() {
+    return HTTP_GET_ACTION;
+  }
+
+  @Override
+  public TCPSocketAction getTcpSocket() {
+    return null;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritSite.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritSite.java
new file mode 100644
index 0000000..2b604c8
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritSite.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit;
+
+import io.fabric8.kubernetes.api.model.Quantity;
+import java.io.Serializable;
+
+public class GerritSite implements Serializable {
+  private static final long serialVersionUID = 1L;
+  Quantity size;
+
+  public Quantity getSize() {
+    return size;
+  }
+
+  public void setSize(Quantity size) {
+    this.size = size;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritSpec.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritSpec.java
new file mode 100644
index 0000000..ed21087
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritSpec.java
@@ -0,0 +1,74 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.ContainerImageConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GerritStorageConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GlobalRefDbConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.IngressConfig;
+
+public class GerritSpec extends GerritTemplateSpec {
+  private ContainerImageConfig containerImages = new ContainerImageConfig();
+  private GerritStorageConfig storage = new GerritStorageConfig();
+  private IngressConfig ingress = new IngressConfig();
+  private GlobalRefDbConfig refdb = new GlobalRefDbConfig();
+  private String serverId = "";
+
+  public GerritSpec() {}
+
+  public GerritSpec(GerritTemplateSpec templateSpec) {
+    super(templateSpec);
+  }
+
+  public ContainerImageConfig getContainerImages() {
+    return containerImages;
+  }
+
+  public void setContainerImages(ContainerImageConfig containerImages) {
+    this.containerImages = containerImages;
+  }
+
+  public GerritStorageConfig getStorage() {
+    return storage;
+  }
+
+  public void setStorage(GerritStorageConfig storage) {
+    this.storage = storage;
+  }
+
+  public IngressConfig getIngress() {
+    return ingress;
+  }
+
+  public void setIngress(IngressConfig ingress) {
+    this.ingress = ingress;
+  }
+
+  public GlobalRefDbConfig getRefdb() {
+    return refdb;
+  }
+
+  public void setRefdb(GlobalRefDbConfig refdb) {
+    this.refdb = refdb;
+  }
+
+  public String getServerId() {
+    return serverId;
+  }
+
+  public void setServerId(String serverId) {
+    this.serverId = serverId;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritStatus.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritStatus.java
new file mode 100644
index 0000000..f779f75
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritStatus.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class GerritStatus {
+  private boolean ready = false;
+  private Map<String, String> appliedConfigMapVersions = new HashMap<>();
+  private Map<String, String> appliedSecretVersions = new HashMap<>();
+
+  public boolean isReady() {
+    return ready;
+  }
+
+  public void setReady(boolean ready) {
+    this.ready = ready;
+  }
+
+  public Map<String, String> getAppliedConfigMapVersions() {
+    return appliedConfigMapVersions;
+  }
+
+  public void setAppliedConfigMapVersions(Map<String, String> appliedConfigMapVersions) {
+    this.appliedConfigMapVersions = appliedConfigMapVersions;
+  }
+
+  public Map<String, String> getAppliedSecretVersions() {
+    return appliedSecretVersions;
+  }
+
+  public void setAppliedSecretVersions(Map<String, String> appliedSecretVersions) {
+    this.appliedSecretVersions = appliedSecretVersions;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritTemplate.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritTemplate.java
new file mode 100644
index 0000000..1144737
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritTemplate.java
@@ -0,0 +1,108 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GlobalRefDbConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.IngressConfig;
+import io.fabric8.kubernetes.api.model.KubernetesResource;
+import io.fabric8.kubernetes.api.model.ObjectMeta;
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+
+@JsonDeserialize(using = com.fasterxml.jackson.databind.JsonDeserializer.None.class)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonPropertyOrder({"metadata", "spec"})
+public class GerritTemplate implements KubernetesResource {
+  private static final long serialVersionUID = 1L;
+
+  @JsonProperty("metadata")
+  private ObjectMeta metadata;
+
+  @JsonProperty("spec")
+  private GerritTemplateSpec spec;
+
+  public GerritTemplate() {}
+
+  @JsonProperty("metadata")
+  public ObjectMeta getMetadata() {
+    return metadata;
+  }
+
+  @JsonProperty("metadata")
+  public void setMetadata(ObjectMeta metadata) {
+    this.metadata = metadata;
+  }
+
+  @JsonProperty("spec")
+  public GerritTemplateSpec getSpec() {
+    return spec;
+  }
+
+  @JsonProperty("spec")
+  public void setSpec(GerritTemplateSpec spec) {
+    this.spec = spec;
+  }
+
+  @JsonIgnore
+  public Gerrit toGerrit(GerritCluster gerritCluster) {
+    Gerrit gerrit = new Gerrit();
+    gerrit.setMetadata(getGerritMetadata(gerritCluster));
+    GerritSpec gerritSpec = new GerritSpec(spec);
+    gerritSpec.setContainerImages(gerritCluster.getSpec().getContainerImages());
+    gerritSpec.setStorage(gerritCluster.getSpec().getStorage());
+    IngressConfig ingressConfig = new IngressConfig();
+    ingressConfig.setEnabled(gerritCluster.getSpec().getIngress().isEnabled());
+    ingressConfig.setHost(gerritCluster.getSpec().getIngress().getHost());
+    ingressConfig.setTlsEnabled(gerritCluster.getSpec().getIngress().getTls().isEnabled());
+    ingressConfig.setSsh(gerritCluster.getSpec().getIngress().getSsh());
+    gerritSpec.setIngress(ingressConfig);
+    gerritSpec.setServerId(getServerId(gerritCluster));
+    if (getSpec().isHighlyAvailablePrimary()) {
+      GlobalRefDbConfig refdb = gerritCluster.getSpec().getRefdb();
+      if (refdb.getZookeeper() != null && refdb.getZookeeper().getRootNode() == null) {
+        refdb
+            .getZookeeper()
+            .setRootNode(
+                gerritCluster.getMetadata().getNamespace()
+                    + "/"
+                    + gerritCluster.getMetadata().getName());
+      }
+      gerritSpec.setRefdb(gerritCluster.getSpec().getRefdb());
+    }
+    gerrit.setSpec(gerritSpec);
+    return gerrit;
+  }
+
+  @JsonIgnore
+  private ObjectMeta getGerritMetadata(GerritCluster gerritCluster) {
+    return new ObjectMetaBuilder()
+        .withName(metadata.getName())
+        .withLabels(metadata.getLabels())
+        .withNamespace(gerritCluster.getMetadata().getNamespace())
+        .build();
+  }
+
+  private String getServerId(GerritCluster gerritCluster) {
+    String serverId = gerritCluster.getSpec().getServerId();
+    return serverId.isBlank()
+        ? gerritCluster.getMetadata().getNamespace() + "/" + gerritCluster.getMetadata().getName()
+        : serverId;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritTemplateSpec.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritTemplateSpec.java
new file mode 100644
index 0000000..4349cd1
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritTemplateSpec.java
@@ -0,0 +1,259 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.HttpSshServiceConfig;
+import io.fabric8.kubernetes.api.model.Affinity;
+import io.fabric8.kubernetes.api.model.ResourceRequirements;
+import io.fabric8.kubernetes.api.model.Toleration;
+import io.fabric8.kubernetes.api.model.TopologySpreadConstraint;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class GerritTemplateSpec {
+  private String serviceAccount;
+
+  private List<Toleration> tolerations;
+  private Affinity affinity;
+  private List<TopologySpreadConstraint> topologySpreadConstraints = new ArrayList<>();
+  private String priorityClassName;
+
+  private int replicas = 1;
+  private int updatePartition = 0;
+
+  private ResourceRequirements resources;
+
+  private GerritProbe startupProbe = new GerritProbe();
+  private GerritProbe readinessProbe = new GerritProbe();
+  private GerritProbe livenessProbe = new GerritProbe();
+
+  private long gracefulStopTimeout = 30L;
+
+  private HttpSshServiceConfig service = new HttpSshServiceConfig();
+
+  private GerritSite site = new GerritSite();
+  private List<GerritPlugin> plugins = List.of();
+  private List<GerritModule> libs = List.of();
+  private Map<String, String> configFiles = Map.of();
+  private String secretRef;
+  private GerritMode mode = GerritMode.PRIMARY;
+
+  private GerritDebugConfig debug = new GerritDebugConfig();
+
+  public GerritTemplateSpec() {}
+
+  public GerritTemplateSpec(GerritTemplateSpec templateSpec) {
+    this.serviceAccount = templateSpec.serviceAccount;
+    this.tolerations = templateSpec.tolerations;
+    this.affinity = templateSpec.affinity;
+    this.topologySpreadConstraints = templateSpec.topologySpreadConstraints;
+    this.priorityClassName = templateSpec.priorityClassName;
+
+    this.replicas = templateSpec.replicas;
+    this.updatePartition = templateSpec.updatePartition;
+
+    this.resources = templateSpec.resources;
+
+    this.startupProbe = templateSpec.startupProbe;
+    this.readinessProbe = templateSpec.readinessProbe;
+    this.livenessProbe = templateSpec.livenessProbe;
+
+    this.gracefulStopTimeout = templateSpec.gracefulStopTimeout;
+
+    this.service = templateSpec.service;
+
+    this.site = templateSpec.site;
+    this.plugins = templateSpec.plugins;
+    this.libs = templateSpec.libs;
+    this.configFiles = templateSpec.configFiles;
+    this.secretRef = templateSpec.secretRef;
+    this.mode = templateSpec.mode;
+
+    this.debug = templateSpec.debug;
+  }
+
+  public String getServiceAccount() {
+    return serviceAccount;
+  }
+
+  public void setServiceAccount(String serviceAccount) {
+    this.serviceAccount = serviceAccount;
+  }
+
+  public List<Toleration> getTolerations() {
+    return tolerations;
+  }
+
+  public void setTolerations(List<Toleration> tolerations) {
+    this.tolerations = tolerations;
+  }
+
+  public Affinity getAffinity() {
+    return affinity;
+  }
+
+  public void setAffinity(Affinity affinity) {
+    this.affinity = affinity;
+  }
+
+  public List<TopologySpreadConstraint> getTopologySpreadConstraints() {
+    return topologySpreadConstraints;
+  }
+
+  public void setTopologySpreadConstraints(
+      List<TopologySpreadConstraint> topologySpreadConstraints) {
+    this.topologySpreadConstraints = topologySpreadConstraints;
+  }
+
+  public String getPriorityClassName() {
+    return priorityClassName;
+  }
+
+  public void setPriorityClassName(String priorityClassName) {
+    this.priorityClassName = priorityClassName;
+  }
+
+  public int getReplicas() {
+    return replicas;
+  }
+
+  public void setReplicas(int replicas) {
+    this.replicas = replicas;
+  }
+
+  public int getUpdatePartition() {
+    return updatePartition;
+  }
+
+  public void setUpdatePartition(int updatePartition) {
+    this.updatePartition = updatePartition;
+  }
+
+  public ResourceRequirements getResources() {
+    return resources;
+  }
+
+  public void setResources(ResourceRequirements resources) {
+    this.resources = resources;
+  }
+
+  public GerritProbe getStartupProbe() {
+    return startupProbe;
+  }
+
+  public void setStartupProbe(GerritProbe startupProbe) {
+    this.startupProbe = startupProbe;
+  }
+
+  public GerritProbe getReadinessProbe() {
+    return readinessProbe;
+  }
+
+  public void setReadinessProbe(GerritProbe readinessProbe) {
+    this.readinessProbe = readinessProbe;
+  }
+
+  public GerritProbe getLivenessProbe() {
+    return livenessProbe;
+  }
+
+  public void setLivenessProbe(GerritProbe livenessProbe) {
+    this.livenessProbe = livenessProbe;
+  }
+
+  public long getGracefulStopTimeout() {
+    return gracefulStopTimeout;
+  }
+
+  public void setGracefulStopTimeout(long gracefulStopTimeout) {
+    this.gracefulStopTimeout = gracefulStopTimeout;
+  }
+
+  public HttpSshServiceConfig getService() {
+    return service;
+  }
+
+  public void setService(HttpSshServiceConfig service) {
+    this.service = service;
+  }
+
+  public GerritSite getSite() {
+    return site;
+  }
+
+  public void setSite(GerritSite site) {
+    this.site = site;
+  }
+
+  public List<GerritPlugin> getPlugins() {
+    return plugins;
+  }
+
+  public void setPlugins(List<GerritPlugin> plugins) {
+    this.plugins = plugins;
+  }
+
+  public List<GerritModule> getLibs() {
+    return libs;
+  }
+
+  public void setLibs(List<GerritModule> libs) {
+    this.libs = libs;
+  }
+
+  public Map<String, String> getConfigFiles() {
+    return configFiles;
+  }
+
+  public void setConfigFiles(Map<String, String> configFiles) {
+    this.configFiles = configFiles;
+  }
+
+  public String getSecretRef() {
+    return secretRef;
+  }
+
+  public void setSecretRef(String secretRef) {
+    this.secretRef = secretRef;
+  }
+
+  public GerritMode getMode() {
+    return mode;
+  }
+
+  public void setMode(GerritMode mode) {
+    this.mode = mode;
+  }
+
+  public GerritDebugConfig getDebug() {
+    return debug;
+  }
+
+  public void setDebug(GerritDebugConfig debug) {
+    this.debug = debug;
+  }
+
+  public enum GerritMode {
+    PRIMARY,
+    REPLICA
+  }
+
+  @JsonIgnore
+  public boolean isHighlyAvailablePrimary() {
+    return getMode().equals(GerritMode.PRIMARY) && getReplicas() > 1;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gitgc/GitGarbageCollection.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gitgc/GitGarbageCollection.java
new file mode 100644
index 0000000..7d268bd
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gitgc/GitGarbageCollection.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.gitgc;
+
+import io.fabric8.kubernetes.api.model.Namespaced;
+import io.fabric8.kubernetes.client.CustomResource;
+import io.fabric8.kubernetes.model.annotation.Group;
+import io.fabric8.kubernetes.model.annotation.Plural;
+import io.fabric8.kubernetes.model.annotation.ShortNames;
+import io.fabric8.kubernetes.model.annotation.Version;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+@Group("gerritoperator.google.com")
+@Version("v1alpha1")
+@ShortNames("gitgc")
+@Plural("gitgcs")
+public class GitGarbageCollection
+    extends CustomResource<GitGarbageCollectionSpec, GitGarbageCollectionStatus>
+    implements Namespaced {
+  private static final long serialVersionUID = 1L;
+
+  public String toString() {
+    return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE);
+  }
+
+  @Override
+  protected GitGarbageCollectionStatus initStatus() {
+    return new GitGarbageCollectionStatus();
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gitgc/GitGarbageCollectionSpec.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gitgc/GitGarbageCollectionSpec.java
new file mode 100644
index 0000000..7648f13
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gitgc/GitGarbageCollectionSpec.java
@@ -0,0 +1,102 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.gitgc;
+
+import io.fabric8.kubernetes.api.model.Affinity;
+import io.fabric8.kubernetes.api.model.ResourceRequirements;
+import io.fabric8.kubernetes.api.model.Toleration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+public class GitGarbageCollectionSpec {
+  private String cluster;
+  private String schedule;
+  private Set<String> projects;
+  private ResourceRequirements resources;
+  private List<Toleration> tolerations;
+  private Affinity affinity;
+
+  public GitGarbageCollectionSpec() {
+    resources = new ResourceRequirements();
+    projects = new HashSet<>();
+  }
+
+  public String getCluster() {
+    return cluster;
+  }
+
+  public void setCluster(String cluster) {
+    this.cluster = cluster;
+  }
+
+  public void setSchedule(String schedule) {
+    this.schedule = schedule;
+  }
+
+  public String getSchedule() {
+    return schedule;
+  }
+
+  public Set<String> getProjects() {
+    return projects;
+  }
+
+  public void setProjects(Set<String> projects) {
+    this.projects = projects;
+  }
+
+  public void setResources(ResourceRequirements resources) {
+    this.resources = resources;
+  }
+
+  public ResourceRequirements getResources() {
+    return resources;
+  }
+
+  public List<Toleration> getTolerations() {
+    return tolerations;
+  }
+
+  public void setTolerations(List<Toleration> tolerations) {
+    this.tolerations = tolerations;
+  }
+
+  public Affinity getAffinity() {
+    return affinity;
+  }
+
+  public void setAffinity(Affinity affinity) {
+    this.affinity = affinity;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(cluster, projects, resources, schedule);
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj instanceof GitGarbageCollectionSpec) {
+      GitGarbageCollectionSpec other = (GitGarbageCollectionSpec) obj;
+      return Objects.equals(cluster, other.cluster)
+          && Objects.equals(projects, other.projects)
+          && Objects.equals(resources, other.resources)
+          && Objects.equals(schedule, other.schedule);
+    }
+    return false;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gitgc/GitGarbageCollectionStatus.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gitgc/GitGarbageCollectionStatus.java
new file mode 100644
index 0000000..ddff4c7
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gitgc/GitGarbageCollectionStatus.java
@@ -0,0 +1,76 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.gitgc;
+
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+public class GitGarbageCollectionStatus {
+  private boolean replicateAll = false;
+  private Set<String> excludedProjects = new HashSet<>();
+  private GitGcState state = GitGcState.INACTIVE;
+
+  public boolean isReplicateAll() {
+    return replicateAll;
+  }
+
+  public void setReplicateAll(boolean replicateAll) {
+    this.replicateAll = replicateAll;
+  }
+
+  public Set<String> getExcludedProjects() {
+    return excludedProjects;
+  }
+
+  public void resetExcludedProjects() {
+    excludedProjects = new HashSet<>();
+  }
+
+  public void excludeProjects(Set<String> projects) {
+    excludedProjects.addAll(projects);
+  }
+
+  public GitGcState getState() {
+    return state;
+  }
+
+  public void setState(GitGcState state) {
+    this.state = state;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(excludedProjects, replicateAll, state);
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj instanceof GitGarbageCollectionStatus) {
+      GitGarbageCollectionStatus other = (GitGarbageCollectionStatus) obj;
+      return Objects.equals(excludedProjects, other.excludedProjects)
+          && replicateAll == other.replicateAll
+          && state == other.state;
+    }
+    return false;
+  }
+
+  public enum GitGcState {
+    ACTIVE,
+    INACTIVE,
+    CONFLICT,
+    ERROR
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/network/GerritNetwork.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/network/GerritNetwork.java
new file mode 100644
index 0000000..7596f54
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/network/GerritNetwork.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.network;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import io.fabric8.kubernetes.api.model.Namespaced;
+import io.fabric8.kubernetes.api.model.Status;
+import io.fabric8.kubernetes.client.CustomResource;
+import io.fabric8.kubernetes.model.annotation.Group;
+import io.fabric8.kubernetes.model.annotation.ShortNames;
+import io.fabric8.kubernetes.model.annotation.Version;
+
+@Group("gerritoperator.google.com")
+@Version("v1alpha2")
+@ShortNames("gn")
+public class GerritNetwork extends CustomResource<GerritNetworkSpec, Status> implements Namespaced {
+  private static final long serialVersionUID = 1L;
+
+  public static final String SESSION_COOKIE_NAME = "Gerrit_Session";
+  public static final String SESSION_COOKIE_TTL = "60s";
+
+  @JsonIgnore
+  public String getDependentResourceName(String nameSuffix) {
+    return String.format("%s-%s", getMetadata().getName(), nameSuffix);
+  }
+
+  @JsonIgnore
+  public boolean hasPrimaryGerrit() {
+    return getSpec().getPrimaryGerrit() != null;
+  }
+
+  @JsonIgnore
+  public boolean hasGerritReplica() {
+    return getSpec().getGerritReplica() != null;
+  }
+
+  @JsonIgnore
+  public boolean hasGerrits() {
+    return hasGerritReplica() || hasPrimaryGerrit();
+  }
+
+  @JsonIgnore
+  public boolean hasReceiver() {
+    return getSpec().getReceiver() != null;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/network/GerritNetworkSpec.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/network/GerritNetworkSpec.java
new file mode 100644
index 0000000..406341f
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/network/GerritNetworkSpec.java
@@ -0,0 +1,69 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.network;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GerritClusterIngressConfig;
+import java.util.ArrayList;
+import java.util.List;
+
+public class GerritNetworkSpec {
+  private GerritClusterIngressConfig ingress = new GerritClusterIngressConfig();
+  private NetworkMember receiver;
+  private NetworkMemberWithSsh primaryGerrit;
+  private NetworkMemberWithSsh gerritReplica;
+
+  public GerritClusterIngressConfig getIngress() {
+    return ingress;
+  }
+
+  public void setIngress(GerritClusterIngressConfig ingress) {
+    this.ingress = ingress;
+  }
+
+  public NetworkMember getReceiver() {
+    return receiver;
+  }
+
+  public void setReceiver(NetworkMember receiver) {
+    this.receiver = receiver;
+  }
+
+  public NetworkMemberWithSsh getPrimaryGerrit() {
+    return primaryGerrit;
+  }
+
+  public void setPrimaryGerrit(NetworkMemberWithSsh primaryGerrit) {
+    this.primaryGerrit = primaryGerrit;
+  }
+
+  public NetworkMemberWithSsh getGerritReplica() {
+    return gerritReplica;
+  }
+
+  public void setGerritReplica(NetworkMemberWithSsh gerritReplica) {
+    this.gerritReplica = gerritReplica;
+  }
+
+  public List<NetworkMemberWithSsh> getGerrits() {
+    List<NetworkMemberWithSsh> gerrits = new ArrayList<>();
+    if (primaryGerrit != null) {
+      gerrits.add(primaryGerrit);
+    }
+    if (gerritReplica != null) {
+      gerrits.add(gerritReplica);
+    }
+    return gerrits;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/network/NetworkMember.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/network/NetworkMember.java
new file mode 100644
index 0000000..433dbbe
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/network/NetworkMember.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.network;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.HttpServiceConfig;
+
+public class NetworkMember {
+  private String name;
+  private int httpPort = 8080;
+
+  public NetworkMember() {}
+
+  public NetworkMember(String name, HttpServiceConfig serviceConfig) {
+    this.name = name;
+    this.httpPort = serviceConfig.getHttpPort();
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public void setName(String name) {
+    this.name = name;
+  }
+
+  public int getHttpPort() {
+    return httpPort;
+  }
+
+  public void setHttpPort(int httpPort) {
+    this.httpPort = httpPort;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/network/NetworkMemberWithSsh.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/network/NetworkMemberWithSsh.java
new file mode 100644
index 0000000..1668739
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/network/NetworkMemberWithSsh.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.network;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.HttpSshServiceConfig;
+
+public class NetworkMemberWithSsh extends NetworkMember {
+  private int sshPort = 29418;
+
+  public NetworkMemberWithSsh() {}
+
+  public NetworkMemberWithSsh(String name, HttpSshServiceConfig serviceConfig) {
+    super(name, serviceConfig);
+    this.sshPort = serviceConfig.getSshPort();
+  }
+
+  public int getSshPort() {
+    return sshPort;
+  }
+
+  public void setSshPort(int sshPort) {
+    this.sshPort = sshPort;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/Receiver.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/Receiver.java
new file mode 100644
index 0000000..bc546df
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/Receiver.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.receiver;
+
+import io.fabric8.kubernetes.api.model.Namespaced;
+import io.fabric8.kubernetes.client.CustomResource;
+import io.fabric8.kubernetes.model.annotation.Group;
+import io.fabric8.kubernetes.model.annotation.ShortNames;
+import io.fabric8.kubernetes.model.annotation.Version;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+@Group("gerritoperator.google.com")
+@Version("v1alpha6")
+@ShortNames("grec")
+public class Receiver extends CustomResource<ReceiverSpec, ReceiverStatus> implements Namespaced {
+  private static final long serialVersionUID = 1L;
+
+  public String toString() {
+    return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE);
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/ReceiverProbe.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/ReceiverProbe.java
new file mode 100644
index 0000000..78aaa58
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/ReceiverProbe.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.receiver;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.google.gerrit.k8s.operator.receiver.dependent.ReceiverDeployment;
+import io.fabric8.kubernetes.api.model.ExecAction;
+import io.fabric8.kubernetes.api.model.GRPCAction;
+import io.fabric8.kubernetes.api.model.HTTPGetAction;
+import io.fabric8.kubernetes.api.model.IntOrString;
+import io.fabric8.kubernetes.api.model.Probe;
+import io.fabric8.kubernetes.api.model.TCPSocketAction;
+import io.fabric8.kubernetes.api.model.TCPSocketActionBuilder;
+
+public class ReceiverProbe extends Probe {
+  private static final long serialVersionUID = 1L;
+
+  private static final TCPSocketAction TCP_SOCKET_ACTION =
+      new TCPSocketActionBuilder().withPort(new IntOrString(ReceiverDeployment.HTTP_PORT)).build();
+
+  @JsonIgnore private ExecAction exec;
+
+  @JsonIgnore private GRPCAction grpc;
+
+  @JsonIgnore private TCPSocketAction tcpSocket;
+
+  @Override
+  public void setExec(ExecAction exec) {
+    super.setExec(null);
+  }
+
+  @Override
+  public void setGrpc(GRPCAction grpc) {
+    super.setGrpc(null);
+  }
+
+  @Override
+  public void setHttpGet(HTTPGetAction httpGet) {
+    super.setHttpGet(null);
+  }
+
+  @Override
+  public void setTcpSocket(TCPSocketAction tcpSocket) {
+    super.setTcpSocket(TCP_SOCKET_ACTION);
+  }
+
+  @Override
+  public ExecAction getExec() {
+    return null;
+  }
+
+  @Override
+  public GRPCAction getGrpc() {
+    return null;
+  }
+
+  @Override
+  public HTTPGetAction getHttpGet() {
+    return null;
+  }
+
+  @Override
+  public TCPSocketAction getTcpSocket() {
+    return TCP_SOCKET_ACTION;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/ReceiverSpec.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/ReceiverSpec.java
new file mode 100644
index 0000000..005fa49
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/ReceiverSpec.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.receiver;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.ContainerImageConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.IngressConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.StorageConfig;
+
+public class ReceiverSpec extends ReceiverTemplateSpec {
+  private ContainerImageConfig containerImages = new ContainerImageConfig();
+  private StorageConfig storage = new StorageConfig();
+  private IngressConfig ingress = new IngressConfig();
+
+  public ReceiverSpec() {}
+
+  public ReceiverSpec(ReceiverTemplateSpec templateSpec) {
+    super(templateSpec);
+  }
+
+  public ContainerImageConfig getContainerImages() {
+    return containerImages;
+  }
+
+  public void setContainerImages(ContainerImageConfig containerImages) {
+    this.containerImages = containerImages;
+  }
+
+  public StorageConfig getStorage() {
+    return storage;
+  }
+
+  public void setStorage(StorageConfig storage) {
+    this.storage = storage;
+  }
+
+  public IngressConfig getIngress() {
+    return ingress;
+  }
+
+  public void setIngress(IngressConfig ingress) {
+    this.ingress = ingress;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/ReceiverStatus.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/ReceiverStatus.java
new file mode 100644
index 0000000..915a62f
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/ReceiverStatus.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.receiver;
+
+public class ReceiverStatus {
+  private boolean ready;
+  private String appliedCredentialSecretVersion = "";
+
+  public boolean isReady() {
+    return ready;
+  }
+
+  public void setReady(boolean ready) {
+    this.ready = ready;
+  }
+
+  public String getAppliedCredentialSecretVersion() {
+    return appliedCredentialSecretVersion;
+  }
+
+  public void setAppliedCredentialSecretVersion(String appliedCredentialSecretVersion) {
+    this.appliedCredentialSecretVersion = appliedCredentialSecretVersion;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/ReceiverTemplate.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/ReceiverTemplate.java
new file mode 100644
index 0000000..f559419
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/ReceiverTemplate.java
@@ -0,0 +1,86 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.receiver;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.IngressConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.StorageConfig;
+import io.fabric8.kubernetes.api.model.KubernetesResource;
+import io.fabric8.kubernetes.api.model.ObjectMeta;
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+
+@JsonDeserialize(using = com.fasterxml.jackson.databind.JsonDeserializer.None.class)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonPropertyOrder({"metadata", "spec"})
+public class ReceiverTemplate implements KubernetesResource {
+  private static final long serialVersionUID = 1L;
+
+  @JsonProperty("metadata")
+  private ObjectMeta metadata;
+
+  @JsonProperty("spec")
+  private ReceiverTemplateSpec spec;
+
+  public ReceiverTemplate() {}
+
+  @JsonProperty("metadata")
+  public ObjectMeta getMetadata() {
+    return metadata;
+  }
+
+  @JsonProperty("metadata")
+  public void setMetadata(ObjectMeta metadata) {
+    this.metadata = metadata;
+  }
+
+  @JsonProperty("spec")
+  public ReceiverTemplateSpec getSpec() {
+    return spec;
+  }
+
+  @JsonProperty("spec")
+  public void setSpec(ReceiverTemplateSpec spec) {
+    this.spec = spec;
+  }
+
+  @JsonIgnore
+  public Receiver toReceiver(GerritCluster gerritCluster) {
+    Receiver receiver = new Receiver();
+    receiver.setMetadata(getReceiverMetadata(gerritCluster));
+    ReceiverSpec receiverSpec = new ReceiverSpec(spec);
+    receiverSpec.setContainerImages(gerritCluster.getSpec().getContainerImages());
+    receiverSpec.setStorage(new StorageConfig(gerritCluster.getSpec().getStorage()));
+    IngressConfig ingressConfig = new IngressConfig();
+    ingressConfig.setHost(gerritCluster.getSpec().getIngress().getHost());
+    ingressConfig.setTlsEnabled(gerritCluster.getSpec().getIngress().getTls().isEnabled());
+    receiverSpec.setIngress(ingressConfig);
+    receiver.setSpec(receiverSpec);
+    return receiver;
+  }
+
+  @JsonIgnore
+  private ObjectMeta getReceiverMetadata(GerritCluster gerritCluster) {
+    return new ObjectMetaBuilder()
+        .withName(metadata.getName())
+        .withLabels(metadata.getLabels())
+        .withNamespace(gerritCluster.getMetadata().getNamespace())
+        .build();
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/ReceiverTemplateSpec.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/ReceiverTemplateSpec.java
new file mode 100644
index 0000000..22ee904
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/ReceiverTemplateSpec.java
@@ -0,0 +1,163 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.receiver;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.HttpServiceConfig;
+import io.fabric8.kubernetes.api.model.Affinity;
+import io.fabric8.kubernetes.api.model.IntOrString;
+import io.fabric8.kubernetes.api.model.ResourceRequirements;
+import io.fabric8.kubernetes.api.model.Toleration;
+import io.fabric8.kubernetes.api.model.TopologySpreadConstraint;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ReceiverTemplateSpec {
+  private List<Toleration> tolerations = new ArrayList<>();
+  private Affinity affinity;
+  private List<TopologySpreadConstraint> topologySpreadConstraints = new ArrayList<>();
+  private String priorityClassName;
+
+  private int replicas = 1;
+  private IntOrString maxSurge = new IntOrString(1);
+  private IntOrString maxUnavailable = new IntOrString(1);
+
+  private ResourceRequirements resources;
+
+  private ReceiverProbe readinessProbe = new ReceiverProbe();
+  private ReceiverProbe livenessProbe = new ReceiverProbe();
+
+  private HttpServiceConfig service = new HttpServiceConfig();
+
+  private String credentialSecretRef;
+
+  public ReceiverTemplateSpec() {}
+
+  public ReceiverTemplateSpec(ReceiverTemplateSpec templateSpec) {
+    this.tolerations = templateSpec.tolerations;
+    this.affinity = templateSpec.affinity;
+    this.topologySpreadConstraints = templateSpec.topologySpreadConstraints;
+    this.priorityClassName = templateSpec.priorityClassName;
+
+    this.replicas = templateSpec.replicas;
+
+    this.resources = templateSpec.resources;
+    this.maxSurge = templateSpec.maxSurge;
+    this.maxUnavailable = templateSpec.maxUnavailable;
+
+    this.readinessProbe = templateSpec.readinessProbe;
+    this.livenessProbe = templateSpec.livenessProbe;
+
+    this.service = templateSpec.service;
+
+    this.credentialSecretRef = templateSpec.credentialSecretRef;
+  }
+
+  public List<Toleration> getTolerations() {
+    return tolerations;
+  }
+
+  public void setTolerations(List<Toleration> tolerations) {
+    this.tolerations = tolerations;
+  }
+
+  public Affinity getAffinity() {
+    return affinity;
+  }
+
+  public void setAffinity(Affinity affinity) {
+    this.affinity = affinity;
+  }
+
+  public List<TopologySpreadConstraint> getTopologySpreadConstraints() {
+    return topologySpreadConstraints;
+  }
+
+  public void setTopologySpreadConstraints(
+      List<TopologySpreadConstraint> topologySpreadConstraints) {
+    this.topologySpreadConstraints = topologySpreadConstraints;
+  }
+
+  public String getPriorityClassName() {
+    return priorityClassName;
+  }
+
+  public void setPriorityClassName(String priorityClassName) {
+    this.priorityClassName = priorityClassName;
+  }
+
+  public int getReplicas() {
+    return replicas;
+  }
+
+  public void setReplicas(int replicas) {
+    this.replicas = replicas;
+  }
+
+  public IntOrString getMaxSurge() {
+    return maxSurge;
+  }
+
+  public void setMaxSurge(IntOrString maxSurge) {
+    this.maxSurge = maxSurge;
+  }
+
+  public IntOrString getMaxUnavailable() {
+    return maxUnavailable;
+  }
+
+  public void setMaxUnavailable(IntOrString maxUnavailable) {
+    this.maxUnavailable = maxUnavailable;
+  }
+
+  public ResourceRequirements getResources() {
+    return resources;
+  }
+
+  public void setResources(ResourceRequirements resources) {
+    this.resources = resources;
+  }
+
+  public ReceiverProbe getReadinessProbe() {
+    return readinessProbe;
+  }
+
+  public void setReadinessProbe(ReceiverProbe readinessProbe) {
+    this.readinessProbe = readinessProbe;
+  }
+
+  public ReceiverProbe getLivenessProbe() {
+    return livenessProbe;
+  }
+
+  public void setLivenessProbe(ReceiverProbe livenessProbe) {
+    this.livenessProbe = livenessProbe;
+  }
+
+  public HttpServiceConfig getService() {
+    return service;
+  }
+
+  public void setService(HttpServiceConfig service) {
+    this.service = service;
+  }
+
+  public String getCredentialSecretRef() {
+    return credentialSecretRef;
+  }
+
+  public void setCredentialSecretRef(String credentialSecretRef) {
+    this.credentialSecretRef = credentialSecretRef;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/BusyBoxImage.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/BusyBoxImage.java
new file mode 100644
index 0000000..5c5388f
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/BusyBoxImage.java
@@ -0,0 +1,62 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+public class BusyBoxImage {
+  private String registry;
+  private String tag;
+
+  public BusyBoxImage() {
+    this.registry = "docker.io";
+    this.tag = "latest";
+  }
+
+  public void setRegistry(String registry) {
+    this.registry = registry;
+  }
+
+  public String getRegistry() {
+    return registry;
+  }
+
+  public void setTag(String tag) {
+    this.tag = tag;
+  }
+
+  public String getTag() {
+    return tag;
+  }
+
+  @JsonIgnore
+  public String getBusyBoxImage() {
+    StringBuilder builder = new StringBuilder();
+
+    if (registry != null) {
+      builder.append(registry);
+      builder.append("/");
+    }
+
+    builder.append("busybox");
+
+    if (tag != null) {
+      builder.append(":");
+      builder.append(tag);
+    }
+
+    return builder.toString();
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/ContainerImageConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/ContainerImageConfig.java
new file mode 100644
index 0000000..4f84824
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/ContainerImageConfig.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+import io.fabric8.kubernetes.api.model.LocalObjectReference;
+import java.util.HashSet;
+import java.util.Set;
+
+public class ContainerImageConfig {
+  private String imagePullPolicy = "Always";
+  private Set<LocalObjectReference> imagePullSecrets = new HashSet<>();
+  private BusyBoxImage busyBox = new BusyBoxImage();
+  private GerritRepositoryConfig gerritImages = new GerritRepositoryConfig();
+
+  public String getImagePullPolicy() {
+    return imagePullPolicy;
+  }
+
+  public void setImagePullPolicy(String imagePullPolicy) {
+    this.imagePullPolicy = imagePullPolicy;
+  }
+
+  public Set<LocalObjectReference> getImagePullSecrets() {
+    return imagePullSecrets;
+  }
+
+  public void setImagePullSecrets(Set<LocalObjectReference> imagePullSecrets) {
+    this.imagePullSecrets = imagePullSecrets;
+  }
+
+  public BusyBoxImage getBusyBox() {
+    return busyBox;
+  }
+
+  public void setBusyBox(BusyBoxImage busyBox) {
+    this.busyBox = busyBox;
+  }
+
+  public GerritRepositoryConfig getGerritImages() {
+    return gerritImages;
+  }
+
+  public void setGerritImages(GerritRepositoryConfig gerritImages) {
+    this.gerritImages = gerritImages;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritClusterIngressConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritClusterIngressConfig.java
new file mode 100644
index 0000000..a915820
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritClusterIngressConfig.java
@@ -0,0 +1,85 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import java.util.Map;
+
+public class GerritClusterIngressConfig {
+  private boolean enabled = false;
+  private String host;
+  private Map<String, String> annotations;
+  private GerritIngressTlsConfig tls = new GerritIngressTlsConfig();
+  private GerritIngressSshConfig ssh = new GerritIngressSshConfig();
+  private GerritIngressAmbassadorConfig ambassador = new GerritIngressAmbassadorConfig();
+
+  public boolean isEnabled() {
+    return enabled;
+  }
+
+  public void setEnabled(boolean enabled) {
+    this.enabled = enabled;
+  }
+
+  public String getHost() {
+    return host;
+  }
+
+  public void setHost(String host) {
+    this.host = host;
+  }
+
+  public Map<String, String> getAnnotations() {
+    return annotations;
+  }
+
+  public void setAnnotations(Map<String, String> annotations) {
+    this.annotations = annotations;
+  }
+
+  public GerritIngressTlsConfig getTls() {
+    return tls;
+  }
+
+  public void setTls(GerritIngressTlsConfig tls) {
+    this.tls = tls;
+  }
+
+  public GerritIngressSshConfig getSsh() {
+    return ssh;
+  }
+
+  public void setSsh(GerritIngressSshConfig ssh) {
+    this.ssh = ssh;
+  }
+
+  public GerritIngressAmbassadorConfig getAmbassador() {
+    return ambassador;
+  }
+
+  public void setAmbassador(GerritIngressAmbassadorConfig ambassador) {
+    this.ambassador = ambassador;
+  }
+
+  @JsonIgnore
+  public String getFullHostnameForService(String svcName) {
+    return getFullHostnameForService(svcName, getHost());
+  }
+
+  @JsonIgnore
+  public static String getFullHostnameForService(String svcName, String ingressHost) {
+    return String.format("%s.%s", svcName, ingressHost);
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritIngressAmbassadorConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritIngressAmbassadorConfig.java
new file mode 100644
index 0000000..b34eb67
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritIngressAmbassadorConfig.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+import java.util.List;
+
+public class GerritIngressAmbassadorConfig {
+  private List<String> id;
+  private boolean createHost;
+
+  public List<String> getId() {
+    return this.id;
+  }
+
+  public void setId(List<String> id) {
+    this.id = id;
+  }
+
+  public boolean getCreateHost() {
+    return this.createHost;
+  }
+
+  public void setCreateHost(boolean createHost) {
+    this.createHost = createHost;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritIngressSshConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritIngressSshConfig.java
new file mode 100644
index 0000000..6203803
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritIngressSshConfig.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+public class GerritIngressSshConfig {
+  private boolean enabled = false;
+
+  public boolean isEnabled() {
+    return enabled;
+  }
+
+  public void setEnabled(boolean enabled) {
+    this.enabled = enabled;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritIngressTlsConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritIngressTlsConfig.java
new file mode 100644
index 0000000..b4552f5
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritIngressTlsConfig.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+public class GerritIngressTlsConfig {
+
+  private boolean enabled = false;
+  private String secret;
+
+  public boolean isEnabled() {
+    return enabled;
+  }
+
+  public void setEnabled(boolean enabled) {
+    this.enabled = enabled;
+  }
+
+  public String getSecret() {
+    return secret;
+  }
+
+  public void setSecret(String secret) {
+    this.secret = secret;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritRepositoryConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritRepositoryConfig.java
new file mode 100644
index 0000000..f593c1e
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritRepositoryConfig.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+public class GerritRepositoryConfig {
+  private String registry;
+  private String org;
+  private String tag;
+
+  public GerritRepositoryConfig() {
+    this.registry = "docker.io";
+    this.org = "k8sgerrit";
+    this.tag = "latest";
+  }
+
+  public void setRegistry(String registry) {
+    this.registry = registry;
+  }
+
+  public String getRegistry() {
+    return registry;
+  }
+
+  public String getOrg() {
+    return org;
+  }
+
+  public void setOrg(String org) {
+    this.org = org;
+  }
+
+  public void setTag(String tag) {
+    this.tag = tag;
+  }
+
+  public String getTag() {
+    return tag;
+  }
+
+  @JsonIgnore
+  public String getFullImageName(String image) {
+    StringBuilder builder = new StringBuilder();
+
+    if (registry != null) {
+      builder.append(registry);
+      builder.append("/");
+    }
+
+    if (org != null) {
+      builder.append(org);
+      builder.append("/");
+    }
+
+    builder.append(image);
+
+    if (tag != null) {
+      builder.append(":");
+      builder.append(tag);
+    }
+
+    return builder.toString();
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritStorageConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritStorageConfig.java
new file mode 100644
index 0000000..d66a0cd
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritStorageConfig.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+public class GerritStorageConfig extends StorageConfig {
+  private PluginCacheConfig pluginCache = new PluginCacheConfig();
+
+  public PluginCacheConfig getPluginCache() {
+    return pluginCache;
+  }
+
+  public void setPluginCache(PluginCacheConfig pluginCache) {
+    this.pluginCache = pluginCache;
+  }
+
+  public class PluginCacheConfig {
+    private boolean enabled;
+
+    public boolean isEnabled() {
+      return enabled;
+    }
+
+    public void setEnabled(boolean enabled) {
+      this.enabled = enabled;
+    }
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GlobalRefDbConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GlobalRefDbConfig.java
new file mode 100644
index 0000000..d338555
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GlobalRefDbConfig.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+public class GlobalRefDbConfig {
+  private RefDatabase database = RefDatabase.NONE;
+  private ZookeeperRefDbConfig zookeeper;
+  private SpannerRefDbConfig spanner;
+
+  public RefDatabase getDatabase() {
+    return database;
+  }
+
+  public void setDatabase(RefDatabase database) {
+    this.database = database;
+  }
+
+  public ZookeeperRefDbConfig getZookeeper() {
+    return zookeeper;
+  }
+
+  public void setZookeeper(ZookeeperRefDbConfig zookeeper) {
+    this.zookeeper = zookeeper;
+  }
+
+  public SpannerRefDbConfig getSpanner() {
+    return spanner;
+  }
+
+  public void setSpanner(SpannerRefDbConfig spanner) {
+    this.spanner = spanner;
+  }
+
+  public enum RefDatabase {
+    NONE,
+    ZOOKEEPER,
+    SPANNER,
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/HttpServiceConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/HttpServiceConfig.java
new file mode 100644
index 0000000..8e7d651
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/HttpServiceConfig.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+import java.io.Serializable;
+
+public class HttpServiceConfig implements Serializable {
+  private static final long serialVersionUID = 1L;
+
+  String type = "NodePort";
+  int httpPort = 80;
+
+  public String getType() {
+    return type;
+  }
+
+  public void setType(String type) {
+    this.type = type;
+  }
+
+  public int getHttpPort() {
+    return httpPort;
+  }
+
+  public void setHttpPort(int httpPort) {
+    this.httpPort = httpPort;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/HttpSshServiceConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/HttpSshServiceConfig.java
new file mode 100644
index 0000000..8655c32
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/HttpSshServiceConfig.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+import java.io.Serializable;
+
+public class HttpSshServiceConfig extends HttpServiceConfig implements Serializable {
+  private static final long serialVersionUID = 1L;
+
+  int sshPort = 0;
+
+  public int getSshPort() {
+    return sshPort;
+  }
+
+  public void setSshPort(int sshPort) {
+    this.sshPort = sshPort;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/IngressConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/IngressConfig.java
new file mode 100644
index 0000000..f83b189
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/IngressConfig.java
@@ -0,0 +1,75 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+public class IngressConfig {
+  private boolean enabled;
+  private String host;
+  private boolean tlsEnabled;
+  private GerritIngressSshConfig ssh = new GerritIngressSshConfig();
+
+  public boolean isEnabled() {
+    return enabled;
+  }
+
+  public void setEnabled(boolean enabled) {
+    this.enabled = enabled;
+  }
+
+  public String getHost() {
+    return host;
+  }
+
+  public void setHost(String host) {
+    this.host = host;
+  }
+
+  public boolean isTlsEnabled() {
+    return tlsEnabled;
+  }
+
+  public void setTlsEnabled(boolean tlsEnabled) {
+    this.tlsEnabled = tlsEnabled;
+  }
+
+  public GerritIngressSshConfig getSsh() {
+    return ssh;
+  }
+
+  public void setSsh(GerritIngressSshConfig ssh) {
+    this.ssh = ssh;
+  }
+
+  @JsonIgnore
+  public String getFullHostnameForService(String svcName) {
+    return String.format("%s.%s", svcName, getHost());
+  }
+
+  @JsonIgnore
+  public String getUrl() {
+    String protocol = isTlsEnabled() ? "https" : "http";
+    String hostname = getHost();
+    return String.format("%s://%s", protocol, hostname);
+  }
+
+  @JsonIgnore
+  public String getSshUrl() {
+    String protocol = isTlsEnabled() ? "https" : "http";
+    String hostname = getHost();
+    return String.format("%s://%s", protocol, hostname);
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/NfsWorkaroundConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/NfsWorkaroundConfig.java
new file mode 100644
index 0000000..ed4a4d1
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/NfsWorkaroundConfig.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+public class NfsWorkaroundConfig {
+
+  private boolean enabled = false;
+  private boolean chownOnStartup = false;
+  private String idmapdConfig;
+
+  public boolean isEnabled() {
+    return enabled;
+  }
+
+  public void setEnabled(boolean enabled) {
+    this.enabled = enabled;
+  }
+
+  public boolean isChownOnStartup() {
+    return chownOnStartup;
+  }
+
+  public void setChownOnStartup(boolean chownOnStartup) {
+    this.chownOnStartup = chownOnStartup;
+  }
+
+  public String getIdmapdConfig() {
+    return idmapdConfig;
+  }
+
+  public void setIdmapdConfig(String idmapdConfig) {
+    this.idmapdConfig = idmapdConfig;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/SharedStorage.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/SharedStorage.java
new file mode 100644
index 0000000..d2193c1
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/SharedStorage.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+import io.fabric8.kubernetes.api.model.LabelSelector;
+import io.fabric8.kubernetes.api.model.Quantity;
+
+public class SharedStorage {
+  private ExternalPVCConfig externalPVC = new ExternalPVCConfig();
+  private Quantity size;
+  private String volumeName;
+  private LabelSelector selector;
+
+  public ExternalPVCConfig getExternalPVC() {
+    return externalPVC;
+  }
+
+  public void setExternalPVC(ExternalPVCConfig externalPVC) {
+    this.externalPVC = externalPVC;
+  }
+
+  public Quantity getSize() {
+    return size;
+  }
+
+  public String getVolumeName() {
+    return volumeName;
+  }
+
+  public void setSize(Quantity size) {
+    this.size = size;
+  }
+
+  public void setVolumeName(String volumeName) {
+    this.volumeName = volumeName;
+  }
+
+  public LabelSelector getSelector() {
+    return selector;
+  }
+
+  public void setSelector(LabelSelector selector) {
+    this.selector = selector;
+  }
+
+  public class ExternalPVCConfig {
+    private boolean enabled;
+    private String claimName = "";
+
+    public boolean isEnabled() {
+      return enabled;
+    }
+
+    public void setEnabled(boolean enabled) {
+      this.enabled = enabled;
+    }
+
+    public String getClaimName() {
+      return claimName;
+    }
+
+    public void setClaimName(String claimName) {
+      this.claimName = claimName;
+    }
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/SpannerRefDbConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/SpannerRefDbConfig.java
new file mode 100644
index 0000000..eee7eab
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/SpannerRefDbConfig.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+public class SpannerRefDbConfig {
+  private String projectName;
+  private String instance;
+  private String database;
+
+  public String getProjectName() {
+    return projectName;
+  }
+
+  public void setProjectName(String projectName) {
+    this.projectName = projectName;
+  }
+
+  public String getInstance() {
+    return instance;
+  }
+
+  public void setInstance(String instance) {
+    this.instance = instance;
+  }
+
+  public String getDatabase() {
+    return database;
+  }
+
+  public void setDatabase(String database) {
+    this.database = database;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/StorageClassConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/StorageClassConfig.java
new file mode 100644
index 0000000..de4906b
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/StorageClassConfig.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+public class StorageClassConfig {
+
+  String readWriteOnce = "default";
+  String readWriteMany = "shared-storage";
+  NfsWorkaroundConfig nfsWorkaround = new NfsWorkaroundConfig();
+
+  public String getReadWriteOnce() {
+    return readWriteOnce;
+  }
+
+  public String getReadWriteMany() {
+    return readWriteMany;
+  }
+
+  public void setReadWriteOnce(String readWriteOnce) {
+    this.readWriteOnce = readWriteOnce;
+  }
+
+  public void setReadWriteMany(String readWriteMany) {
+    this.readWriteMany = readWriteMany;
+  }
+
+  public NfsWorkaroundConfig getNfsWorkaround() {
+    return nfsWorkaround;
+  }
+
+  public void setNfsWorkaround(NfsWorkaroundConfig nfsWorkaround) {
+    this.nfsWorkaround = nfsWorkaround;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/StorageConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/StorageConfig.java
new file mode 100644
index 0000000..566ce6e
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/StorageConfig.java
@@ -0,0 +1,44 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+public class StorageConfig {
+
+  private StorageClassConfig storageClasses;
+  private SharedStorage sharedStorage;
+
+  public StorageConfig() {}
+
+  public StorageConfig(GerritStorageConfig gerritStorageConfig) {
+    storageClasses = gerritStorageConfig.getStorageClasses();
+    sharedStorage = gerritStorageConfig.getSharedStorage();
+  }
+
+  public StorageClassConfig getStorageClasses() {
+    return storageClasses;
+  }
+
+  public void setStorageClasses(StorageClassConfig storageClasses) {
+    this.storageClasses = storageClasses;
+  }
+
+  public SharedStorage getSharedStorage() {
+    return sharedStorage;
+  }
+
+  public void setSharedStorage(SharedStorage sharedStorage) {
+    this.sharedStorage = sharedStorage;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/ZookeeperRefDbConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/ZookeeperRefDbConfig.java
new file mode 100644
index 0000000..e90faeb
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/ZookeeperRefDbConfig.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+public class ZookeeperRefDbConfig {
+  private String connectString;
+  private String rootNode;
+
+  public String getConnectString() {
+    return connectString;
+  }
+
+  public void setConnectString(String connectString) {
+    this.connectString = connectString;
+  }
+
+  public String getRootNode() {
+    return rootNode;
+  }
+
+  public void setRootNode(String rootNode) {
+    this.rootNode = rootNode;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/gerrit/config/GerritConfigBuilder.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/gerrit/config/GerritConfigBuilder.java
new file mode 100644
index 0000000..221704d
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/gerrit/config/GerritConfigBuilder.java
@@ -0,0 +1,172 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.gerrit.config;
+
+import static com.google.gerrit.k8s.operator.gerrit.dependent.GerritStatefulSet.HTTP_PORT;
+import static com.google.gerrit.k8s.operator.gerrit.dependent.GerritStatefulSet.SSH_PORT;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.k8s.operator.gerrit.config.ConfigBuilder;
+import com.google.gerrit.k8s.operator.gerrit.config.RequiredOption;
+import com.google.gerrit.k8s.operator.gerrit.dependent.GerritService;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplateSpec.GerritMode;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.IngressConfig;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class GerritConfigBuilder extends ConfigBuilder {
+  private static final Pattern PROTOCOL_PATTERN = Pattern.compile("^(https?)://.+");
+
+  public GerritConfigBuilder(Gerrit gerrit) {
+    super(
+        gerrit.getSpec().getConfigFiles().getOrDefault("gerrit.config", ""),
+        ImmutableList.copyOf(collectRequiredOptions(gerrit)));
+  }
+
+  private static List<RequiredOption<?>> collectRequiredOptions(Gerrit gerrit) {
+    List<RequiredOption<?>> requiredOptions = new ArrayList<>();
+    requiredOptions.addAll(cacheSection(gerrit));
+    requiredOptions.addAll(containerSection(gerrit));
+    requiredOptions.addAll(gerritSection(gerrit));
+    requiredOptions.addAll(httpdSection(gerrit));
+    requiredOptions.addAll(sshdSection(gerrit));
+    return requiredOptions;
+  }
+
+  private static List<RequiredOption<?>> cacheSection(Gerrit gerrit) {
+    List<RequiredOption<?>> requiredOptions = new ArrayList<>();
+    requiredOptions.add(new RequiredOption<String>("cache", "directory", "cache"));
+    return requiredOptions;
+  }
+
+  private static List<RequiredOption<?>> containerSection(Gerrit gerrit) {
+    List<RequiredOption<?>> requiredOptions = new ArrayList<>();
+    requiredOptions.add(new RequiredOption<String>("container", "user", "gerrit"));
+    requiredOptions.add(
+        new RequiredOption<Boolean>(
+            "container", "replica", gerrit.getSpec().getMode().equals(GerritMode.REPLICA)));
+    requiredOptions.add(
+        new RequiredOption<String>("container", "javaHome", "/usr/lib/jvm/java-11-openjdk"));
+    requiredOptions.add(javaOptions(gerrit));
+    return requiredOptions;
+  }
+
+  private static List<RequiredOption<?>> gerritSection(Gerrit gerrit) {
+    List<RequiredOption<?>> requiredOptions = new ArrayList<>();
+    String serverId = gerrit.getSpec().getServerId();
+    requiredOptions.add(new RequiredOption<String>("gerrit", "basepath", "git"));
+    if (serverId != null && !serverId.isBlank()) {
+      requiredOptions.add(new RequiredOption<String>("gerrit", "serverId", serverId));
+    }
+
+    if (gerrit.getSpec().isHighlyAvailablePrimary()) {
+      requiredOptions.add(
+          new RequiredOption<Set<String>>(
+              "gerrit",
+              "installModule",
+              Set.of("com.gerritforge.gerrit.globalrefdb.validation.LibModule")));
+      requiredOptions.add(
+          new RequiredOption<Set<String>>(
+              "gerrit",
+              "installDbModule",
+              Set.of("com.ericsson.gerrit.plugins.highavailability.ValidationModule")));
+    }
+
+    IngressConfig ingressConfig = gerrit.getSpec().getIngress();
+    if (ingressConfig.isEnabled()) {
+      requiredOptions.add(
+          new RequiredOption<String>("gerrit", "canonicalWebUrl", ingressConfig.getUrl()));
+    }
+
+    return requiredOptions;
+  }
+
+  private static List<RequiredOption<?>> httpdSection(Gerrit gerrit) {
+    List<RequiredOption<?>> requiredOptions = new ArrayList<>();
+    IngressConfig ingressConfig = gerrit.getSpec().getIngress();
+    if (ingressConfig.isEnabled()) {
+      requiredOptions.add(listenUrl(ingressConfig.getUrl()));
+    }
+    return requiredOptions;
+  }
+
+  private static List<RequiredOption<?>> sshdSection(Gerrit gerrit) {
+    List<RequiredOption<?>> requiredOptions = new ArrayList<>();
+    requiredOptions.add(sshListenAddress(gerrit));
+    IngressConfig ingressConfig = gerrit.getSpec().getIngress();
+    if (ingressConfig.isEnabled() && gerrit.isSshEnabled()) {
+      requiredOptions.add(sshAdvertisedAddress(gerrit));
+    }
+    return requiredOptions;
+  }
+
+  private static RequiredOption<Set<String>> javaOptions(Gerrit gerrit) {
+    Set<String> javaOptions = new HashSet<>();
+    javaOptions.add("-Djavax.net.ssl.trustStore=/var/gerrit/etc/keystore");
+    if (gerrit.getSpec().isHighlyAvailablePrimary()) {
+      javaOptions.add("-Djava.net.preferIPv4Stack=true");
+    }
+    if (gerrit.getSpec().getDebug().isEnabled()) {
+      javaOptions.add("-Xdebug");
+      String debugServerCfg = "-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000";
+      if (gerrit.getSpec().getDebug().isSuspend()) {
+        debugServerCfg = debugServerCfg + ",suspend=y";
+      } else {
+        debugServerCfg = debugServerCfg + ",suspend=n";
+      }
+      javaOptions.add(debugServerCfg);
+    }
+    return new RequiredOption<Set<String>>("container", "javaOptions", javaOptions);
+  }
+
+  private static RequiredOption<String> listenUrl(String url) {
+    StringBuilder listenUrlBuilder = new StringBuilder();
+    listenUrlBuilder.append("proxy-");
+    Matcher protocolMatcher = PROTOCOL_PATTERN.matcher(url);
+    if (protocolMatcher.matches()) {
+      listenUrlBuilder.append(protocolMatcher.group(1));
+    } else {
+      throw new IllegalStateException(
+          String.format("Unknown protocol used for canonicalWebUrl: %s", url));
+    }
+    listenUrlBuilder.append("://*:");
+    listenUrlBuilder.append(HTTP_PORT);
+    listenUrlBuilder.append("/");
+    return new RequiredOption<String>("httpd", "listenUrl", listenUrlBuilder.toString());
+  }
+
+  private static RequiredOption<String> sshListenAddress(Gerrit gerrit) {
+    String listenAddress;
+    if (gerrit.isSshEnabled()) {
+      listenAddress = "*:" + SSH_PORT;
+    } else {
+      listenAddress = "off";
+    }
+    return new RequiredOption<String>("sshd", "listenAddress", listenAddress);
+  }
+
+  private static RequiredOption<String> sshAdvertisedAddress(Gerrit gerrit) {
+    return new RequiredOption<String>(
+        "sshd",
+        "advertisedAddress",
+        gerrit.getSpec().getIngress().getFullHostnameForService(GerritService.getName(gerrit))
+            + ":29418");
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/gerrit/config/HighAvailabilityPluginConfigBuilder.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/gerrit/config/HighAvailabilityPluginConfigBuilder.java
new file mode 100644
index 0000000..b96a93b
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/gerrit/config/HighAvailabilityPluginConfigBuilder.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.gerrit.config;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.k8s.operator.gerrit.config.ConfigBuilder;
+import com.google.gerrit.k8s.operator.gerrit.config.RequiredOption;
+import com.google.gerrit.k8s.operator.gerrit.dependent.GerritStatefulSet;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class HighAvailabilityPluginConfigBuilder extends ConfigBuilder {
+  public HighAvailabilityPluginConfigBuilder(Gerrit gerrit) {
+    super(
+        gerrit.getSpec().getConfigFiles().getOrDefault("high-availability.config", ""),
+        ImmutableList.copyOf(collectRequiredOptions(gerrit)));
+  }
+
+  private static List<RequiredOption<?>> collectRequiredOptions(Gerrit gerrit) {
+    List<RequiredOption<?>> requiredOptions = new ArrayList<>();
+    requiredOptions.add(new RequiredOption<String>("main", "sharedDirectory", "shared"));
+    requiredOptions.add(new RequiredOption<String>("peerInfo", "strategy", "jgroups"));
+    requiredOptions.add(new RequiredOption<String>("peerInfo", "jgroups", "myUrl", null));
+    requiredOptions.add(
+        new RequiredOption<String>("jgroups", "clusterName", gerrit.getMetadata().getName()));
+    requiredOptions.add(new RequiredOption<Boolean>("jgroups", "kubernetes", true));
+    requiredOptions.add(
+        new RequiredOption<String>(
+            "jgroups", "kubernetes", "namespace", gerrit.getMetadata().getNamespace()));
+    requiredOptions.add(
+        new RequiredOption<Set<String>>("jgroups", "kubernetes", "label", getLabels(gerrit)));
+    requiredOptions.add(new RequiredOption<Boolean>("cache", "synchronize", true));
+    requiredOptions.add(new RequiredOption<Boolean>("event", "synchronize", true));
+    requiredOptions.add(new RequiredOption<Boolean>("index", "synchronize", true));
+    requiredOptions.add(new RequiredOption<Boolean>("index", "synchronizeForced", true));
+    requiredOptions.add(new RequiredOption<Boolean>("healthcheck", "enable", true));
+    requiredOptions.add(new RequiredOption<Boolean>("ref-database", "enabled", true));
+    return requiredOptions;
+  }
+
+  private static Set<String> getLabels(Gerrit gerrit) {
+    Map<String, String> selectorLabels = GerritStatefulSet.getSelectorLabels(gerrit);
+    Set<String> labels = new HashSet<>();
+    for (Map.Entry<String, String> label : selectorLabels.entrySet()) {
+      labels.add(label.getKey() + "=" + label.getValue());
+    }
+    return labels;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/gerrit/config/SpannerRefDbPluginConfigBuilder.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/gerrit/config/SpannerRefDbPluginConfigBuilder.java
new file mode 100644
index 0000000..0f3cb30
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/gerrit/config/SpannerRefDbPluginConfigBuilder.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.gerrit.config;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.k8s.operator.gerrit.config.ConfigBuilder;
+import com.google.gerrit.k8s.operator.gerrit.config.RequiredOption;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import java.util.ArrayList;
+import java.util.List;
+
+public class SpannerRefDbPluginConfigBuilder extends ConfigBuilder {
+  public SpannerRefDbPluginConfigBuilder(Gerrit gerrit) {
+    super(
+        gerrit.getSpec().getConfigFiles().getOrDefault("spanner-refdb.config", ""),
+        ImmutableList.copyOf(collectRequiredOptions(gerrit)));
+  }
+
+  private static List<RequiredOption<?>> collectRequiredOptions(Gerrit gerrit) {
+    List<RequiredOption<?>> requiredOptions = new ArrayList<>();
+    requiredOptions.add(
+        new RequiredOption<String>("ref-database", "spanner", "useEmulator", "false"));
+    requiredOptions.add(
+        new RequiredOption<String>(
+            "ref-database",
+            "spanner",
+            "projectName",
+            gerrit.getSpec().getRefdb().getSpanner().getProjectName()));
+    requiredOptions.add(
+        new RequiredOption<String>(
+            "ref-database", "spanner", "credentialsPath", "/var/gerrit/etc/gcp-credentials.json"));
+    requiredOptions.add(
+        new RequiredOption<String>(
+            "ref-database",
+            "spanner",
+            "instance",
+            gerrit.getSpec().getRefdb().getSpanner().getInstance()));
+    requiredOptions.add(
+        new RequiredOption<String>(
+            "ref-database",
+            "spanner",
+            "database",
+            gerrit.getSpec().getRefdb().getSpanner().getDatabase()));
+    return requiredOptions;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/gerrit/config/ZookeeperRefDbPluginConfigBuilder.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/gerrit/config/ZookeeperRefDbPluginConfigBuilder.java
new file mode 100644
index 0000000..aabb726
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/gerrit/config/ZookeeperRefDbPluginConfigBuilder.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.gerrit.config;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.k8s.operator.gerrit.config.ConfigBuilder;
+import com.google.gerrit.k8s.operator.gerrit.config.RequiredOption;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ZookeeperRefDbPluginConfigBuilder extends ConfigBuilder {
+  public ZookeeperRefDbPluginConfigBuilder(Gerrit gerrit) {
+    super(
+        gerrit.getSpec().getConfigFiles().getOrDefault("zookeeper-refdb.config", ""),
+        ImmutableList.copyOf(collectRequiredOptions(gerrit)));
+  }
+
+  private static List<RequiredOption<?>> collectRequiredOptions(Gerrit gerrit) {
+    List<RequiredOption<?>> requiredOptions = new ArrayList<>();
+    requiredOptions.add(
+        new RequiredOption<String>(
+            "ref-database",
+            "zookeeper",
+            "connectString",
+            gerrit.getSpec().getRefdb().getZookeeper().getConnectString()));
+    requiredOptions.add(
+        new RequiredOption<String>(
+            "ref-database",
+            "zookeeper",
+            "rootNode",
+            gerrit.getSpec().getRefdb().getZookeeper().getRootNode()));
+    return requiredOptions;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/resources/META-INF/services/io.fabric8.kubernetes.api.model.KubernetesResource b/charts/k8s-gerrit/operator/src/main/resources/META-INF/services/io.fabric8.kubernetes.api.model.KubernetesResource
new file mode 100644
index 0000000..6fb15ac
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/resources/META-INF/services/io.fabric8.kubernetes.api.model.KubernetesResource
@@ -0,0 +1,5 @@
+com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster
+com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit
+com.google.gerrit.k8s.operator.v1alpha.api.model.gitgc.GitGarbageCollection
+com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.Receiver
+com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork
\ No newline at end of file
diff --git a/charts/k8s-gerrit/operator/src/main/resources/crd/emissary-crds.yaml b/charts/k8s-gerrit/operator/src/main/resources/crd/emissary-crds.yaml
new file mode 100644
index 0000000..bba936f
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/resources/crd/emissary-crds.yaml
@@ -0,0 +1,2589 @@
+# This file is downloaded from the Emissary repository on GitHub:
+# https://github.com/emissary-ingress/emissary/blob/master/manifests/emissary/emissary-crds.yaml.in
+#
+# Several modifications have been manually made:
+# 1. Only the `Mapping`, `TLSContext`, and `Host` CRDs have been kept from the source file. The source
+#    file defines many CRDs that are not required by this operator project so the unnecessary CRDs have
+#    been deleted.
+# 2. `v2ExplicitTLS` field has been removed from the Mapping CRD `v3alpha1` version. This is because
+#    the "crd-to-java" generator plugin we use has a bug (https://github.com/fabric8io/kubernetes-client/issues/5457)
+#    while converting enum types and the bug is triggered by the `v2ExplicitTLS` field. This field
+#    may be added back in once we upgrade our fabric8 version to 6.8.x, where this bug is resolved.
+# 3. `ambassador_id` property is added to `Mapping`, `TLSContext`, and `Host` CRD version `v2`, by
+#     copying it over from `v3`.
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.12.0
+  labels:
+    app.kubernetes.io/instance: emissary-apiext
+    app.kubernetes.io/managed-by: kubectl_apply_-f_emissary-apiext.yaml
+    app.kubernetes.io/name: emissary-apiext
+    app.kubernetes.io/part-of: emissary-apiext
+  name: mappings.getambassador.io
+spec:
+  conversion:
+    strategy: Webhook
+    webhook:
+      clientConfig:
+        service:
+          name: emissary-apiext
+          namespace: emissary-system
+      conversionReviewVersions:
+      - v1
+  group: getambassador.io
+  names:
+    categories:
+    - ambassador-crds
+    kind: Mapping
+    listKind: MappingList
+    plural: mappings
+    singular: mapping
+  preserveUnknownFields: false
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - jsonPath: .spec.host
+      name: Source Host
+      type: string
+    - jsonPath: .spec.prefix
+      name: Source Prefix
+      type: string
+    - jsonPath: .spec.service
+      name: Dest Service
+      type: string
+    - jsonPath: .status.state
+      name: State
+      type: string
+    - jsonPath: .status.reason
+      name: Reason
+      type: string
+    name: v1
+    schema:
+      openAPIV3Schema:
+        description: Mapping is the Schema for the mappings API
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: MappingSpec defines the desired state of Mapping
+            properties:
+              add_linkerd_headers:
+                type: boolean
+              add_request_headers:
+                type: object
+                x-kubernetes-preserve-unknown-fields: true
+              add_response_headers:
+                type: object
+                x-kubernetes-preserve-unknown-fields: true
+              allow_upgrade:
+                description: "A case-insensitive list of the non-HTTP protocols to
+                  allow \"upgrading\" to from HTTP via the \"Connection: upgrade\"
+                  mechanism[1].  After the upgrade, Ambassador does not interpret
+                  the traffic, and behaves similarly to how it does for TCPMappings.
+                  \n [1]: https://tools.ietf.org/html/rfc7230#section-6.7 \n For example,
+                  if your upstream service supports WebSockets, you would write \n
+                  allow_upgrade: - websocket \n Or if your upstream service supports
+                  upgrading from HTTP to SPDY (as the Kubernetes apiserver does for
+                  `kubectl exec` functionality), you would write \n allow_upgrade:
+                  - spdy/3.1"
+                items:
+                  type: string
+                type: array
+              auth_context_extensions:
+                additionalProperties:
+                  type: string
+                type: object
+              auto_host_rewrite:
+                type: boolean
+              bypass_auth:
+                type: boolean
+              bypass_error_response_overrides:
+                description: If true, bypasses any `error_response_overrides` set
+                  on the Ambassador module.
+                type: boolean
+              case_sensitive:
+                type: boolean
+              circuit_breakers:
+                items:
+                  properties:
+                    max_connections:
+                      type: integer
+                    max_pending_requests:
+                      type: integer
+                    max_requests:
+                      type: integer
+                    max_retries:
+                      type: integer
+                    priority:
+                      enum:
+                      - default
+                      - high
+                      type: string
+                  type: object
+                type: array
+              cluster_idle_timeout_ms:
+                type: integer
+              cluster_max_connection_lifetime_ms:
+                type: integer
+              cluster_tag:
+                type: string
+              connect_timeout_ms:
+                type: integer
+              cors:
+                properties:
+                  credentials:
+                    type: boolean
+                  max_age:
+                    type: string
+                type: object
+                x-kubernetes-preserve-unknown-fields: true
+              dns_type:
+                type: string
+              docs:
+                description: DocsInfo provides some extra information about the docs
+                  for the Mapping (used by the Dev Portal)
+                properties:
+                  display_name:
+                    type: string
+                  ignored:
+                    type: boolean
+                  path:
+                    type: string
+                  timeout_ms:
+                    type: integer
+                  url:
+                    type: string
+                type: object
+              enable_ipv4:
+                type: boolean
+              enable_ipv6:
+                type: boolean
+              envoy_override:
+                type: object
+                x-kubernetes-preserve-unknown-fields: true
+              error_response_overrides:
+                description: Error response overrides for this Mapping. Replaces all
+                  of the `error_response_overrides` set on the Ambassador module,
+                  if any.
+                items:
+                  description: A response rewrite for an HTTP error response
+                  properties:
+                    body:
+                      description: The new response body
+                      properties:
+                        content_type:
+                          description: The content type to set on the error response
+                            body when using text_format or text_format_source. Defaults
+                            to 'text/plain'.
+                          type: string
+                        json_format:
+                          additionalProperties:
+                            type: string
+                          description: 'A JSON response with content-type: application/json.
+                            The values can contain format text like in text_format.'
+                          type: object
+                        text_format:
+                          description: A format string representing a text response
+                            body. Content-Type can be set using the `content_type`
+                            field below.
+                          type: string
+                        text_format_source:
+                          description: A format string sourced from a file on the
+                            Ambassador container. Useful for larger response bodies
+                            that should not be placed inline in configuration.
+                          properties:
+                            filename:
+                              description: The name of a file on the Ambassador pod
+                                that contains a format text string.
+                              type: string
+                          type: object
+                      type: object
+                    on_status_code:
+                      description: The status code to match on -- not a pointer because
+                        it's required.
+                      maximum: 599
+                      minimum: 400
+                      type: integer
+                  required:
+                  - body
+                  - on_status_code
+                  type: object
+                minItems: 1
+                type: array
+              grpc:
+                type: boolean
+              headers:
+                type: object
+                x-kubernetes-preserve-unknown-fields: true
+              host:
+                type: string
+              host_redirect:
+                type: boolean
+              host_regex:
+                type: boolean
+              host_rewrite:
+                type: string
+              idle_timeout_ms:
+                type: integer
+              keepalive:
+                properties:
+                  idle_time:
+                    type: integer
+                  interval:
+                    type: integer
+                  probes:
+                    type: integer
+                type: object
+              labels:
+                additionalProperties:
+                  description: A MappingLabelGroupsArray is an array of MappingLabelGroups.
+                    I know, complex.
+                  items:
+                    description: 'A MappingLabelGroup is a single element of a MappingLabelGroupsArray:
+                      a second map, where the key is a human-readable name that identifies
+                      the group.'
+                    maxProperties: 1
+                    minProperties: 1
+                    type: object
+                    x-kubernetes-preserve-unknown-fields: true
+                  type: array
+                description: A DomainMap is the overall Mapping.spec.Labels type.
+                  It maps domains (kind of like namespaces for Mapping labels) to
+                  arrays of label groups.
+                type: object
+              load_balancer:
+                properties:
+                  cookie:
+                    properties:
+                      name:
+                        type: string
+                      path:
+                        type: string
+                      ttl:
+                        type: string
+                    required:
+                    - name
+                    type: object
+                  header:
+                    type: string
+                  policy:
+                    enum:
+                    - round_robin
+                    - ring_hash
+                    - maglev
+                    - least_request
+                    type: string
+                  source_ip:
+                    type: boolean
+                required:
+                - policy
+                type: object
+              method:
+                type: string
+              method_regex:
+                type: boolean
+              modules:
+                items:
+                  type: object
+                  x-kubernetes-preserve-unknown-fields: true
+                type: array
+              outlier_detection:
+                type: string
+              path_redirect:
+                description: Path replacement to use when generating an HTTP redirect.
+                  Used with `host_redirect`.
+                type: string
+              precedence:
+                type: integer
+              prefix:
+                type: string
+              prefix_exact:
+                type: boolean
+              prefix_redirect:
+                description: Prefix rewrite to use when generating an HTTP redirect.
+                  Used with `host_redirect`.
+                type: string
+              prefix_regex:
+                type: boolean
+              priority:
+                type: string
+              query_parameters:
+                type: object
+                x-kubernetes-preserve-unknown-fields: true
+              redirect_response_code:
+                description: The response code to use when generating an HTTP redirect.
+                  Defaults to 301. Used with `host_redirect`.
+                enum:
+                - 301
+                - 302
+                - 303
+                - 307
+                - 308
+                type: integer
+              regex_headers:
+                additionalProperties:
+                  type: string
+                type: object
+              regex_query_parameters:
+                additionalProperties:
+                  type: string
+                type: object
+              regex_redirect:
+                description: Prefix regex rewrite to use when generating an HTTP redirect.
+                  Used with `host_redirect`.
+                properties:
+                  pattern:
+                    type: string
+                  substitution:
+                    type: string
+                type: object
+              regex_rewrite:
+                properties:
+                  pattern:
+                    type: string
+                  substitution:
+                    type: string
+                type: object
+              resolver:
+                type: string
+              respect_dns_ttl:
+                type: boolean
+              retry_policy:
+                properties:
+                  num_retries:
+                    type: integer
+                  per_try_timeout:
+                    type: string
+                  retry_on:
+                    enum:
+                    - 5xx
+                    - gateway-error
+                    - connect-failure
+                    - retriable-4xx
+                    - refused-stream
+                    - retriable-status-codes
+                    type: string
+                type: object
+              rewrite:
+                type: string
+              service:
+                type: string
+              shadow:
+                type: boolean
+              timeout_ms:
+                description: The timeout for requests that use this Mapping. Overrides
+                  `cluster_request_timeout_ms` set on the Ambassador Module, if it
+                  exists.
+                type: integer
+              use_websocket:
+                description: 'use_websocket is deprecated, and is equivlaent to setting
+                  `allow_upgrade: ["websocket"]`'
+                type: boolean
+              v3StatsName:
+                type: string
+              v3health_checks:
+                items:
+                  description: HealthCheck specifies settings for performing active
+                    health checking on upstreams
+                  properties:
+                    health_check:
+                      description: Configuration for where the healthcheck request
+                        should be made to
+                      maxProperties: 1
+                      minProperties: 1
+                      properties:
+                        grpc:
+                          description: HealthCheck for gRPC upstreams. Only one of
+                            grpc_health_check or http_health_check may be specified
+                          properties:
+                            authority:
+                              description: The value of the :authority header in the
+                                gRPC health check request. If left empty the upstream
+                                name will be used.
+                              type: string
+                            upstream_name:
+                              description: The upstream name parameter which will
+                                be sent to gRPC service in the health check message
+                              type: string
+                          required:
+                          - upstream_name
+                          type: object
+                        http:
+                          description: HealthCheck for HTTP upstreams. Only one of
+                            http_health_check or grpc_health_check may be specified
+                          properties:
+                            add_request_headers:
+                              additionalProperties:
+                                properties:
+                                  append:
+                                    type: boolean
+                                  v2Representation:
+                                    enum:
+                                    - ""
+                                    - string
+                                    - "null"
+                                    type: string
+                                  value:
+                                    type: string
+                                type: object
+                              type: object
+                            expected_statuses:
+                              items:
+                                description: A range of response statuses from Start
+                                  to End inclusive
+                                properties:
+                                  max:
+                                    description: End of the statuses to include. Must
+                                      be between 100 and 599 (inclusive)
+                                    maximum: 599
+                                    minimum: 100
+                                    type: integer
+                                  min:
+                                    description: Start of the statuses to include.
+                                      Must be between 100 and 599 (inclusive)
+                                    maximum: 599
+                                    minimum: 100
+                                    type: integer
+                                required:
+                                - max
+                                - min
+                                type: object
+                              type: array
+                            hostname:
+                              type: string
+                            path:
+                              type: string
+                            remove_request_headers:
+                              items:
+                                type: string
+                              type: array
+                          required:
+                          - path
+                          type: object
+                      type: object
+                    healthy_threshold:
+                      description: Number of expected responses for the upstream to
+                        be considered healthy. Defaults to 1.
+                      type: integer
+                    interval:
+                      description: Interval between health checks. Defaults to every
+                        5 seconds.
+                      type: string
+                    timeout:
+                      description: Timeout for connecting to the health checking endpoint.
+                        Defaults to 3 seconds.
+                      type: string
+                    unhealthy_threshold:
+                      description: Number of non-expected responses for the upstream
+                        to be considered unhealthy. A single 503 will mark the upstream
+                        as unhealthy regardless of the threshold. Defaults to 2.
+                      type: integer
+                  required:
+                  - health_check
+                  type: object
+                minItems: 1
+                type: array
+              weight:
+                type: integer
+            required:
+            - prefix
+            - service
+            type: object
+            x-kubernetes-preserve-unknown-fields: true
+          status:
+            description: MappingStatus defines the observed state of Mapping
+            properties:
+              reason:
+                type: string
+              state:
+                enum:
+                - ""
+                - Inactive
+                - Running
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: false
+    subresources:
+      status: {}
+  - additionalPrinterColumns:
+    - jsonPath: .spec.host
+      name: Source Host
+      type: string
+    - jsonPath: .spec.prefix
+      name: Source Prefix
+      type: string
+    - jsonPath: .spec.service
+      name: Dest Service
+      type: string
+    - jsonPath: .status.state
+      name: State
+      type: string
+    - jsonPath: .status.reason
+      name: Reason
+      type: string
+    name: v2
+    schema:
+      openAPIV3Schema:
+        description: Mapping is the Schema for the mappings API
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: MappingSpec defines the desired state of Mapping
+            properties:
+              add_linkerd_headers:
+                type: boolean
+              add_request_headers:
+                type: object
+                x-kubernetes-preserve-unknown-fields: true
+              add_response_headers:
+                type: object
+                x-kubernetes-preserve-unknown-fields: true
+              allow_upgrade:
+                description: "A case-insensitive list of the non-HTTP protocols to
+                  allow \"upgrading\" to from HTTP via the \"Connection: upgrade\"
+                  mechanism[1].  After the upgrade, Ambassador does not interpret
+                  the traffic, and behaves similarly to how it does for TCPMappings.
+                  \n [1]: https://tools.ietf.org/html/rfc7230#section-6.7 \n For example,
+                  if your upstream service supports WebSockets, you would write \n
+                  allow_upgrade: - websocket \n Or if your upstream service supports
+                  upgrading from HTTP to SPDY (as the Kubernetes apiserver does for
+                  `kubectl exec` functionality), you would write \n allow_upgrade:
+                  - spdy/3.1"
+                items:
+                  type: string
+                type: array
+              # [operator] added manually by coping over from v3alpha1
+              ambassador_id:
+                description: "AmbassadorID declares which Ambassador instances should
+                  pay attention to this resource. If no value is provided, the default
+                  is: \n ambassador_id: - \"default\""
+                items:
+                  type: string
+                type: array
+              auth_context_extensions:
+                additionalProperties:
+                  type: string
+                type: object
+              auto_host_rewrite:
+                type: boolean
+              bypass_auth:
+                type: boolean
+              bypass_error_response_overrides:
+                description: If true, bypasses any `error_response_overrides` set
+                  on the Ambassador module.
+                type: boolean
+              case_sensitive:
+                type: boolean
+              circuit_breakers:
+                items:
+                  properties:
+                    max_connections:
+                      type: integer
+                    max_pending_requests:
+                      type: integer
+                    max_requests:
+                      type: integer
+                    max_retries:
+                      type: integer
+                    priority:
+                      enum:
+                      - default
+                      - high
+                      type: string
+                  type: object
+                type: array
+              cluster_idle_timeout_ms:
+                type: integer
+              cluster_max_connection_lifetime_ms:
+                type: integer
+              cluster_tag:
+                type: string
+              connect_timeout_ms:
+                type: integer
+              cors:
+                properties:
+                  credentials:
+                    type: boolean
+                  max_age:
+                    type: string
+                type: object
+                x-kubernetes-preserve-unknown-fields: true
+              dns_type:
+                type: string
+              docs:
+                description: DocsInfo provides some extra information about the docs
+                  for the Mapping (used by the Dev Portal)
+                properties:
+                  display_name:
+                    type: string
+                  ignored:
+                    type: boolean
+                  path:
+                    type: string
+                  timeout_ms:
+                    type: integer
+                  url:
+                    type: string
+                type: object
+              enable_ipv4:
+                type: boolean
+              enable_ipv6:
+                type: boolean
+              envoy_override:
+                type: object
+                x-kubernetes-preserve-unknown-fields: true
+              error_response_overrides:
+                description: Error response overrides for this Mapping. Replaces all
+                  of the `error_response_overrides` set on the Ambassador module,
+                  if any.
+                items:
+                  description: A response rewrite for an HTTP error response
+                  properties:
+                    body:
+                      description: The new response body
+                      properties:
+                        content_type:
+                          description: The content type to set on the error response
+                            body when using text_format or text_format_source. Defaults
+                            to 'text/plain'.
+                          type: string
+                        json_format:
+                          additionalProperties:
+                            type: string
+                          description: 'A JSON response with content-type: application/json.
+                            The values can contain format text like in text_format.'
+                          type: object
+                        text_format:
+                          description: A format string representing a text response
+                            body. Content-Type can be set using the `content_type`
+                            field below.
+                          type: string
+                        text_format_source:
+                          description: A format string sourced from a file on the
+                            Ambassador container. Useful for larger response bodies
+                            that should not be placed inline in configuration.
+                          properties:
+                            filename:
+                              description: The name of a file on the Ambassador pod
+                                that contains a format text string.
+                              type: string
+                          type: object
+                      type: object
+                    on_status_code:
+                      description: The status code to match on -- not a pointer because
+                        it's required.
+                      maximum: 599
+                      minimum: 400
+                      type: integer
+                  required:
+                  - body
+                  - on_status_code
+                  type: object
+                minItems: 1
+                type: array
+              grpc:
+                type: boolean
+              headers:
+                type: object
+                x-kubernetes-preserve-unknown-fields: true
+              host:
+                type: string
+              host_redirect:
+                type: boolean
+              host_regex:
+                type: boolean
+              host_rewrite:
+                type: string
+              idle_timeout_ms:
+                type: integer
+              keepalive:
+                properties:
+                  idle_time:
+                    type: integer
+                  interval:
+                    type: integer
+                  probes:
+                    type: integer
+                type: object
+              labels:
+                additionalProperties:
+                  description: A MappingLabelGroupsArray is an array of MappingLabelGroups.
+                    I know, complex.
+                  items:
+                    description: 'A MappingLabelGroup is a single element of a MappingLabelGroupsArray:
+                      a second map, where the key is a human-readable name that identifies
+                      the group.'
+                    maxProperties: 1
+                    minProperties: 1
+                    type: object
+                    x-kubernetes-preserve-unknown-fields: true
+                  type: array
+                description: A DomainMap is the overall Mapping.spec.Labels type.
+                  It maps domains (kind of like namespaces for Mapping labels) to
+                  arrays of label groups.
+                type: object
+              load_balancer:
+                properties:
+                  cookie:
+                    properties:
+                      name:
+                        type: string
+                      path:
+                        type: string
+                      ttl:
+                        type: string
+                    required:
+                    - name
+                    type: object
+                  header:
+                    type: string
+                  policy:
+                    enum:
+                    - round_robin
+                    - ring_hash
+                    - maglev
+                    - least_request
+                    type: string
+                  source_ip:
+                    type: boolean
+                required:
+                - policy
+                type: object
+              method:
+                type: string
+              method_regex:
+                type: boolean
+              modules:
+                items:
+                  type: object
+                  x-kubernetes-preserve-unknown-fields: true
+                type: array
+              outlier_detection:
+                type: string
+              path_redirect:
+                description: Path replacement to use when generating an HTTP redirect.
+                  Used with `host_redirect`.
+                type: string
+              precedence:
+                type: integer
+              prefix:
+                type: string
+              prefix_exact:
+                type: boolean
+              prefix_redirect:
+                description: Prefix rewrite to use when generating an HTTP redirect.
+                  Used with `host_redirect`.
+                type: string
+              prefix_regex:
+                type: boolean
+              priority:
+                type: string
+              query_parameters:
+                type: object
+                x-kubernetes-preserve-unknown-fields: true
+              redirect_response_code:
+                description: The response code to use when generating an HTTP redirect.
+                  Defaults to 301. Used with `host_redirect`.
+                enum:
+                - 301
+                - 302
+                - 303
+                - 307
+                - 308
+                type: integer
+              regex_headers:
+                additionalProperties:
+                  type: string
+                type: object
+              regex_query_parameters:
+                additionalProperties:
+                  type: string
+                type: object
+              regex_redirect:
+                description: Prefix regex rewrite to use when generating an HTTP redirect.
+                  Used with `host_redirect`.
+                properties:
+                  pattern:
+                    type: string
+                  substitution:
+                    type: string
+                type: object
+              regex_rewrite:
+                properties:
+                  pattern:
+                    type: string
+                  substitution:
+                    type: string
+                type: object
+              resolver:
+                type: string
+              respect_dns_ttl:
+                type: boolean
+              retry_policy:
+                properties:
+                  num_retries:
+                    type: integer
+                  per_try_timeout:
+                    type: string
+                  retry_on:
+                    enum:
+                    - 5xx
+                    - gateway-error
+                    - connect-failure
+                    - retriable-4xx
+                    - refused-stream
+                    - retriable-status-codes
+                    type: string
+                type: object
+              rewrite:
+                type: string
+              service:
+                type: string
+              shadow:
+                type: boolean
+              timeout_ms:
+                description: The timeout for requests that use this Mapping. Overrides
+                  `cluster_request_timeout_ms` set on the Ambassador Module, if it
+                  exists.
+                type: integer
+              use_websocket:
+                description: 'use_websocket is deprecated, and is equivlaent to setting
+                  `allow_upgrade: ["websocket"]`'
+                type: boolean
+              v3StatsName:
+                type: string
+              v3health_checks:
+                items:
+                  description: HealthCheck specifies settings for performing active
+                    health checking on upstreams
+                  properties:
+                    health_check:
+                      description: Configuration for where the healthcheck request
+                        should be made to
+                      maxProperties: 1
+                      minProperties: 1
+                      properties:
+                        grpc:
+                          description: HealthCheck for gRPC upstreams. Only one of
+                            grpc_health_check or http_health_check may be specified
+                          properties:
+                            authority:
+                              description: The value of the :authority header in the
+                                gRPC health check request. If left empty the upstream
+                                name will be used.
+                              type: string
+                            upstream_name:
+                              description: The upstream name parameter which will
+                                be sent to gRPC service in the health check message
+                              type: string
+                          required:
+                          - upstream_name
+                          type: object
+                        http:
+                          description: HealthCheck for HTTP upstreams. Only one of
+                            http_health_check or grpc_health_check may be specified
+                          properties:
+                            add_request_headers:
+                              additionalProperties:
+                                properties:
+                                  append:
+                                    type: boolean
+                                  v2Representation:
+                                    enum:
+                                    - ""
+                                    - string
+                                    - "null"
+                                    type: string
+                                  value:
+                                    type: string
+                                type: object
+                              type: object
+                            expected_statuses:
+                              items:
+                                description: A range of response statuses from Start
+                                  to End inclusive
+                                properties:
+                                  max:
+                                    description: End of the statuses to include. Must
+                                      be between 100 and 599 (inclusive)
+                                    maximum: 599
+                                    minimum: 100
+                                    type: integer
+                                  min:
+                                    description: Start of the statuses to include.
+                                      Must be between 100 and 599 (inclusive)
+                                    maximum: 599
+                                    minimum: 100
+                                    type: integer
+                                required:
+                                - max
+                                - min
+                                type: object
+                              type: array
+                            hostname:
+                              type: string
+                            path:
+                              type: string
+                            remove_request_headers:
+                              items:
+                                type: string
+                              type: array
+                          required:
+                          - path
+                          type: object
+                      type: object
+                    healthy_threshold:
+                      description: Number of expected responses for the upstream to
+                        be considered healthy. Defaults to 1.
+                      type: integer
+                    interval:
+                      description: Interval between health checks. Defaults to every
+                        5 seconds.
+                      type: string
+                    timeout:
+                      description: Timeout for connecting to the health checking endpoint.
+                        Defaults to 3 seconds.
+                      type: string
+                    unhealthy_threshold:
+                      description: Number of non-expected responses for the upstream
+                        to be considered unhealthy. A single 503 will mark the upstream
+                        as unhealthy regardless of the threshold. Defaults to 2.
+                      type: integer
+                  required:
+                  - health_check
+                  type: object
+                minItems: 1
+                type: array
+              weight:
+                type: integer
+            required:
+            - prefix
+            - service
+            type: object
+            x-kubernetes-preserve-unknown-fields: true
+          status:
+            description: MappingStatus defines the observed state of Mapping
+            properties:
+              reason:
+                type: string
+              state:
+                enum:
+                - ""
+                - Inactive
+                - Running
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+  - additionalPrinterColumns:
+    - jsonPath: .spec.host
+      name: Source Host
+      type: string
+    - jsonPath: .spec.prefix
+      name: Source Prefix
+      type: string
+    - jsonPath: .spec.service
+      name: Dest Service
+      type: string
+    - jsonPath: .status.state
+      name: State
+      type: string
+    - jsonPath: .status.reason
+      name: Reason
+      type: string
+    name: v3alpha1
+    schema:
+      openAPIV3Schema:
+        description: Mapping is the Schema for the mappings API
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: MappingSpec defines the desired state of Mapping
+            properties:
+              add_linkerd_headers:
+                type: boolean
+              add_request_headers:
+                additionalProperties:
+                  properties:
+                    append:
+                      type: boolean
+                    v2Representation:
+                      enum:
+                      - ""
+                      - string
+                      - "null"
+                      type: string
+                    value:
+                      type: string
+                  type: object
+                type: object
+              add_response_headers:
+                additionalProperties:
+                  properties:
+                    append:
+                      type: boolean
+                    v2Representation:
+                      enum:
+                      - ""
+                      - string
+                      - "null"
+                      type: string
+                    value:
+                      type: string
+                  type: object
+                type: object
+              allow_upgrade:
+                description: "A case-insensitive list of the non-HTTP protocols to
+                  allow \"upgrading\" to from HTTP via the \"Connection: upgrade\"
+                  mechanism[1].  After the upgrade, Ambassador does not interpret
+                  the traffic, and behaves similarly to how it does for TCPMappings.
+                  \n [1]: https://tools.ietf.org/html/rfc7230#section-6.7 \n For example,
+                  if your upstream service supports WebSockets, you would write \n
+                  allow_upgrade: - websocket \n Or if your upstream service supports
+                  upgrading from HTTP to SPDY (as the Kubernetes apiserver does for
+                  `kubectl exec` functionality), you would write \n allow_upgrade:
+                  - spdy/3.1"
+                items:
+                  type: string
+                type: array
+              ambassador_id:
+                description: "AmbassadorID declares which Ambassador instances should
+                  pay attention to this resource. If no value is provided, the default
+                  is: \n ambassador_id: - \"default\""
+                items:
+                  type: string
+                type: array
+              auth_context_extensions:
+                additionalProperties:
+                  type: string
+                type: object
+              auto_host_rewrite:
+                type: boolean
+              bypass_auth:
+                type: boolean
+              bypass_error_response_overrides:
+                description: If true, bypasses any `error_response_overrides` set
+                  on the Ambassador module.
+                type: boolean
+              case_sensitive:
+                type: boolean
+              circuit_breakers:
+                items:
+                  properties:
+                    max_connections:
+                      type: integer
+                    max_pending_requests:
+                      type: integer
+                    max_requests:
+                      type: integer
+                    max_retries:
+                      type: integer
+                    priority:
+                      enum:
+                      - default
+                      - high
+                      type: string
+                  type: object
+                type: array
+              cluster_idle_timeout_ms:
+                type: integer
+              cluster_max_connection_lifetime_ms:
+                type: integer
+              cluster_tag:
+                type: string
+              connect_timeout_ms:
+                type: integer
+              cors:
+                properties:
+                  credentials:
+                    type: boolean
+                  exposed_headers:
+                    items:
+                      type: string
+                    type: array
+                  headers:
+                    items:
+                      type: string
+                    type: array
+                  max_age:
+                    type: string
+                  methods:
+                    items:
+                      type: string
+                    type: array
+                  origins:
+                    items:
+                      type: string
+                    type: array
+                  v2CommaSeparatedOrigins:
+                    type: boolean
+                type: object
+              dns_type:
+                type: string
+              docs:
+                description: DocsInfo provides some extra information about the docs
+                  for the Mapping. Docs is used by both the agent and the DevPortal.
+                properties:
+                  display_name:
+                    type: string
+                  ignored:
+                    type: boolean
+                  path:
+                    type: string
+                  timeout_ms:
+                    type: integer
+                  url:
+                    type: string
+                type: object
+              enable_ipv4:
+                type: boolean
+              enable_ipv6:
+                type: boolean
+              envoy_override:
+                type: object
+                x-kubernetes-preserve-unknown-fields: true
+              error_response_overrides:
+                description: Error response overrides for this Mapping. Replaces all
+                  of the `error_response_overrides` set on the Ambassador module,
+                  if any.
+                items:
+                  description: A response rewrite for an HTTP error response
+                  properties:
+                    body:
+                      description: The new response body
+                      properties:
+                        content_type:
+                          description: The content type to set on the error response
+                            body when using text_format or text_format_source. Defaults
+                            to 'text/plain'.
+                          type: string
+                        json_format:
+                          additionalProperties:
+                            type: string
+                          description: 'A JSON response with content-type: application/json.
+                            The values can contain format text like in text_format.'
+                          type: object
+                        text_format:
+                          description: A format string representing a text response
+                            body. Content-Type can be set using the `content_type`
+                            field below.
+                          type: string
+                        text_format_source:
+                          description: A format string sourced from a file on the
+                            Ambassador container. Useful for larger response bodies
+                            that should not be placed inline in configuration.
+                          properties:
+                            filename:
+                              description: The name of a file on the Ambassador pod
+                                that contains a format text string.
+                              type: string
+                          type: object
+                      type: object
+                    on_status_code:
+                      description: The status code to match on -- not a pointer because
+                        it's required.
+                      maximum: 599
+                      minimum: 400
+                      type: integer
+                  required:
+                  - body
+                  - on_status_code
+                  type: object
+                minItems: 1
+                type: array
+              grpc:
+                type: boolean
+              headers:
+                additionalProperties:
+                  type: string
+                type: object
+              health_checks:
+                items:
+                  description: HealthCheck specifies settings for performing active
+                    health checking on upstreams
+                  properties:
+                    health_check:
+                      description: Configuration for where the healthcheck request
+                        should be made to
+                      maxProperties: 1
+                      minProperties: 1
+                      properties:
+                        grpc:
+                          description: HealthCheck for gRPC upstreams. Only one of
+                            grpc_health_check or http_health_check may be specified
+                          properties:
+                            authority:
+                              description: The value of the :authority header in the
+                                gRPC health check request. If left empty the upstream
+                                name will be used.
+                              type: string
+                            upstream_name:
+                              description: The upstream name parameter which will
+                                be sent to gRPC service in the health check message
+                              type: string
+                          required:
+                          - upstream_name
+                          type: object
+                        http:
+                          description: HealthCheck for HTTP upstreams. Only one of
+                            http_health_check or grpc_health_check may be specified
+                          properties:
+                            add_request_headers:
+                              additionalProperties:
+                                properties:
+                                  append:
+                                    type: boolean
+                                  v2Representation:
+                                    enum:
+                                    - ""
+                                    - string
+                                    - "null"
+                                    type: string
+                                  value:
+                                    type: string
+                                type: object
+                              type: object
+                            expected_statuses:
+                              items:
+                                description: A range of response statuses from Start
+                                  to End inclusive
+                                properties:
+                                  max:
+                                    description: End of the statuses to include. Must
+                                      be between 100 and 599 (inclusive)
+                                    maximum: 599
+                                    minimum: 100
+                                    type: integer
+                                  min:
+                                    description: Start of the statuses to include.
+                                      Must be between 100 and 599 (inclusive)
+                                    maximum: 599
+                                    minimum: 100
+                                    type: integer
+                                required:
+                                - max
+                                - min
+                                type: object
+                              type: array
+                            hostname:
+                              type: string
+                            path:
+                              type: string
+                            remove_request_headers:
+                              items:
+                                type: string
+                              type: array
+                          required:
+                          - path
+                          type: object
+                      type: object
+                    healthy_threshold:
+                      description: Number of expected responses for the upstream to
+                        be considered healthy. Defaults to 1.
+                      type: integer
+                    interval:
+                      description: Interval between health checks. Defaults to every
+                        5 seconds.
+                      type: string
+                    timeout:
+                      description: Timeout for connecting to the health checking endpoint.
+                        Defaults to 3 seconds.
+                      type: string
+                    unhealthy_threshold:
+                      description: Number of non-expected responses for the upstream
+                        to be considered unhealthy. A single 503 will mark the upstream
+                        as unhealthy regardless of the threshold. Defaults to 2.
+                      type: integer
+                  required:
+                  - health_check
+                  type: object
+                minItems: 1
+                type: array
+              host:
+                description: "Exact match for the hostname of a request if HostRegex
+                  is false; regex match for the hostname if HostRegex is true. \n
+                  Host specifies both a match for the ':authority' header of a request,
+                  as well as a match criterion for Host CRDs: a Mapping that specifies
+                  Host will not associate with a Host that doesn't have a matching
+                  Hostname. \n If both Host and Hostname are set, an error is logged,
+                  Host is ignored, and Hostname is used. \n DEPRECATED: Host is either
+                  an exact match or a regex, depending on HostRegex. Use HostName
+                  instead."
+                type: string
+              host_redirect:
+                type: boolean
+              host_regex:
+                description: 'DEPRECATED: Host is either an exact match or a regex,
+                  depending on HostRegex. Use HostName instead.'
+                type: boolean
+              host_rewrite:
+                type: string
+              hostname:
+                description: "Hostname is a DNS glob specifying the hosts to which
+                  this Mapping applies. \n Hostname specifies both a match for the
+                  ':authority' header of a request, as well as a match criterion for
+                  Host CRDs: a Mapping that specifies Hostname will not associate
+                  with a Host that doesn't have a matching Hostname. \n If both Host
+                  and Hostname are set, an error is logged, Host is ignored, and Hostname
+                  is used."
+                type: string
+              idle_timeout_ms:
+                type: integer
+              keepalive:
+                properties:
+                  idle_time:
+                    type: integer
+                  interval:
+                    type: integer
+                  probes:
+                    type: integer
+                type: object
+              labels:
+                additionalProperties:
+                  description: A MappingLabelGroupsArray is an array of MappingLabelGroups.
+                    I know, complex.
+                  items:
+                    additionalProperties:
+                      description: 'A MappingLabelsArray is the value in the MappingLabelGroup:
+                        an array of label specifiers.'
+                      items:
+                        description: "A MappingLabelSpecifier (finally!) defines a
+                          single label. \n This mimics envoy/config/route/v3/route_components.proto:RateLimit:Action:action_specifier."
+                        maxProperties: 1
+                        minProperties: 1
+                        properties:
+                          destination_cluster:
+                            description: Sets the label "destination_cluster=«Envoy
+                              destination cluster name»".
+                            properties:
+                              key:
+                                enum:
+                                - destination_cluster
+                                type: string
+                            required:
+                            - key
+                            type: object
+                          generic_key:
+                            description: Sets the label "«key»=«value»" (where by
+                              default «key» is "generic_key").
+                            properties:
+                              key:
+                                description: The default is "generic_key".
+                                type: string
+                              v2Shorthand:
+                                type: boolean
+                              value:
+                                type: string
+                            required:
+                            - value
+                            type: object
+                          remote_address:
+                            description: Sets the label "remote_address=«IP address
+                              of the client»".
+                            properties:
+                              key:
+                                enum:
+                                - remote_address
+                                type: string
+                            required:
+                            - key
+                            type: object
+                          request_headers:
+                            description: If the «header_name» header is set, then
+                              set the label "«key»=«Value of the «header_name» header»";
+                              otherwise skip applying this label group.
+                            properties:
+                              header_name:
+                                type: string
+                              key:
+                                type: string
+                              omit_if_not_present:
+                                type: boolean
+                            required:
+                            - header_name
+                            - key
+                            type: object
+                          source_cluster:
+                            description: Sets the label "source_cluster=«Envoy source
+                              cluster name»".
+                            properties:
+                              key:
+                                enum:
+                                - source_cluster
+                                type: string
+                            required:
+                            - key
+                            type: object
+                        type: object
+                      type: array
+                    description: 'A MappingLabelGroup is a single element of a MappingLabelGroupsArray:
+                      a second map, where the key is a human-readable name that identifies
+                      the group.'
+                    maxProperties: 1
+                    minProperties: 1
+                    type: object
+                  type: array
+                description: A DomainMap is the overall Mapping.spec.Labels type.
+                  It maps domains (kind of like namespaces for Mapping labels) to
+                  arrays of label groups.
+                type: object
+              load_balancer:
+                properties:
+                  cookie:
+                    properties:
+                      name:
+                        type: string
+                      path:
+                        type: string
+                      ttl:
+                        type: string
+                    required:
+                    - name
+                    type: object
+                  header:
+                    type: string
+                  policy:
+                    enum:
+                    - round_robin
+                    - ring_hash
+                    - maglev
+                    - least_request
+                    type: string
+                  source_ip:
+                    type: boolean
+                required:
+                - policy
+                type: object
+              method:
+                type: string
+              method_regex:
+                type: boolean
+              modules:
+                items:
+                  type: object
+                  x-kubernetes-preserve-unknown-fields: true
+                type: array
+              outlier_detection:
+                type: string
+              path_redirect:
+                description: Path replacement to use when generating an HTTP redirect.
+                  Used with `host_redirect`.
+                type: string
+              precedence:
+                type: integer
+              prefix:
+                type: string
+              prefix_exact:
+                type: boolean
+              prefix_redirect:
+                description: Prefix rewrite to use when generating an HTTP redirect.
+                  Used with `host_redirect`.
+                type: string
+              prefix_regex:
+                type: boolean
+              priority:
+                type: string
+              query_parameters:
+                additionalProperties:
+                  type: string
+                type: object
+              redirect_response_code:
+                description: The response code to use when generating an HTTP redirect.
+                  Defaults to 301. Used with `host_redirect`.
+                enum:
+                - 301
+                - 302
+                - 303
+                - 307
+                - 308
+                type: integer
+              regex_headers:
+                additionalProperties:
+                  type: string
+                type: object
+              regex_query_parameters:
+                additionalProperties:
+                  type: string
+                type: object
+              regex_redirect:
+                description: Prefix regex rewrite to use when generating an HTTP redirect.
+                  Used with `host_redirect`.
+                properties:
+                  pattern:
+                    type: string
+                  substitution:
+                    type: string
+                type: object
+              regex_rewrite:
+                properties:
+                  pattern:
+                    type: string
+                  substitution:
+                    type: string
+                type: object
+              remove_request_headers:
+                items:
+                  type: string
+                type: array
+              remove_response_headers:
+                items:
+                  type: string
+                type: array
+              resolver:
+                type: string
+              respect_dns_ttl:
+                type: boolean
+              retry_policy:
+                properties:
+                  num_retries:
+                    type: integer
+                  per_try_timeout:
+                    type: string
+                  retry_on:
+                    enum:
+                    - 5xx
+                    - gateway-error
+                    - connect-failure
+                    - retriable-4xx
+                    - refused-stream
+                    - retriable-status-codes
+                    type: string
+                type: object
+              rewrite:
+                type: string
+              service:
+                type: string
+              shadow:
+                type: boolean
+              stats_name:
+                type: string
+              timeout_ms:
+                description: The timeout for requests that use this Mapping. Overrides
+                  `cluster_request_timeout_ms` set on the Ambassador Module, if it
+                  exists.
+                type: integer
+              tls:
+                type: string
+              use_websocket:
+                description: 'use_websocket is deprecated, and is equivlaent to setting
+                  `allow_upgrade: ["websocket"]`'
+                type: boolean
+              v2BoolHeaders:
+                items:
+                  type: string
+                type: array
+              v2BoolQueryParameters:
+                items:
+                  type: string
+                type: array
+              # TODO: uncomment when [bug](https://github.com/fabric8io/kubernetes-client/issues/5457) is resolved
+              # v2ExplicitTLS:
+              #   description: V2ExplicitTLS controls some vanity/stylistic elements
+              #     when converting from v3alpha1 to v2.  The values in an V2ExplicitTLS
+              #     should not in any way affect the runtime operation of Emissary;
+              #     except that it may affect internal names in the Envoy config, which
+              #     may in turn affect stats names.  But it should not affect any end-user
+              #     observable behavior.
+              #   properties:
+              #     serviceScheme:
+              #       description: "ServiceScheme specifies how to spell and capitalize
+              #         the scheme-part of the service URL. \n Acceptable values are
+              #         \"http://\" (case-insensitive), \"https://\" (case-insensitive),
+              #         or \"\".  The value is used if it agrees with whether or not
+              #         this resource enables TLS origination, or if something else
+              #         in the resource overrides the scheme."
+              #       pattern: ^([hH][tT][tT][pP][sS]?://)?$
+              #       type: string
+              #     tls:
+              #       description: "TLS controls whether and how to represent the \"tls\"
+              #         field when its value could be implied by the \"service\" field.
+              #         \ In v2, there were a lot of different ways to spell an \"empty\"
+              #         value, and this field specifies which way to spell it (and will
+              #         therefore only be used if the value will indeed be empty). \n
+              #         | Value        | Representation                        | Meaning
+              #         of representation          | |--------------+---------------------------------------+------------------------------------|
+              #         | \"\"           | omit the field                        | defer
+              #         to service (no TLSContext)   | | \"null\"       | store an explicit
+              #         \"null\" in the field | defer to service (no TLSContext)   |
+              #         | \"string\"     | store an empty string in the field    | defer
+              #         to service (no TLSContext)   | | \"bool:false\" | store a Boolean
+              #         \"false\" in the field  | defer to service (no TLSContext)   |
+              #         | \"bool:true\"  | store a Boolean \"true\" in the field   |
+              #         originate TLS (no TLSContext)      | \n If the meaning of the
+              #         representation contradicts anything else (if a TLSContext is
+              #         to be used, or in the case of \"bool:true\" if TLS is not to
+              #         be originated), then this field is ignored."
+              #       enum:
+              #       - ""
+              #       - "null"
+              #       - bool:true
+              #       - bool:false
+              #       - string
+              #       type: string
+              #   type: object
+              weight:
+                type: integer
+            required:
+            - prefix
+            - service
+            type: object
+          status:
+            description: MappingStatus defines the observed state of Mapping
+            properties:
+              reason:
+                type: string
+              state:
+                enum:
+                - ""
+                - Inactive
+                - Running
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: false
+    subresources:
+      status: {}
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.12.0
+  labels:
+    app.kubernetes.io/instance: emissary-apiext
+    app.kubernetes.io/managed-by: kubectl_apply_-f_emissary-apiext.yaml
+    app.kubernetes.io/name: emissary-apiext
+    app.kubernetes.io/part-of: emissary-apiext
+  name: tlscontexts.getambassador.io
+spec:
+  conversion:
+    strategy: Webhook
+    webhook:
+      clientConfig:
+        service:
+          name: emissary-apiext
+          namespace: emissary-system
+      conversionReviewVersions:
+      - v1
+  group: getambassador.io
+  names:
+    categories:
+    - ambassador-crds
+    kind: TLSContext
+    listKind: TLSContextList
+    plural: tlscontexts
+    singular: tlscontext
+  preserveUnknownFields: false
+  scope: Namespaced
+  versions:
+  - name: v1
+    schema:
+      openAPIV3Schema:
+        description: TLSContext is the Schema for the tlscontexts API
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: TLSContextSpec defines the desired state of TLSContext
+            properties:
+              alpn_protocols:
+                type: string
+              ca_secret:
+                type: string
+              cacert_chain_file:
+                type: string
+              cert_chain_file:
+                type: string
+              cert_required:
+                type: boolean
+              cipher_suites:
+                items:
+                  type: string
+                type: array
+              ecdh_curves:
+                items:
+                  type: string
+                type: array
+              hosts:
+                items:
+                  type: string
+                type: array
+              max_tls_version:
+                enum:
+                - v1.0
+                - v1.1
+                - v1.2
+                - v1.3
+                type: string
+              min_tls_version:
+                enum:
+                - v1.0
+                - v1.1
+                - v1.2
+                - v1.3
+                type: string
+              private_key_file:
+                type: string
+              redirect_cleartext_from:
+                type: integer
+              secret:
+                type: string
+              secret_namespacing:
+                type: boolean
+              sni:
+                type: string
+              v3CRLSecret:
+                type: string
+            type: object
+            x-kubernetes-preserve-unknown-fields: true
+        type: object
+    served: true
+    storage: false
+  - name: v2
+    schema:
+      openAPIV3Schema:
+        description: TLSContext is the Schema for the tlscontexts API
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: TLSContextSpec defines the desired state of TLSContext
+            properties:
+              alpn_protocols:
+                type: string
+              # [operator] added manually by coping over from v3alpha1
+              ambassador_id:
+                description: "AmbassadorID declares which Ambassador instances should
+                  pay attention to this resource. If no value is provided, the default
+                  is: \n ambassador_id: - \"default\""
+                items:
+                  type: string
+                type: array
+              ca_secret:
+                type: string
+              cacert_chain_file:
+                type: string
+              cert_chain_file:
+                type: string
+              cert_required:
+                type: boolean
+              cipher_suites:
+                items:
+                  type: string
+                type: array
+              ecdh_curves:
+                items:
+                  type: string
+                type: array
+              hosts:
+                items:
+                  type: string
+                type: array
+              max_tls_version:
+                enum:
+                - v1.0
+                - v1.1
+                - v1.2
+                - v1.3
+                type: string
+              min_tls_version:
+                enum:
+                - v1.0
+                - v1.1
+                - v1.2
+                - v1.3
+                type: string
+              private_key_file:
+                type: string
+              redirect_cleartext_from:
+                type: integer
+              secret:
+                type: string
+              secret_namespacing:
+                type: boolean
+              sni:
+                type: string
+              v3CRLSecret:
+                type: string
+            type: object
+            x-kubernetes-preserve-unknown-fields: true
+        type: object
+    served: true
+    storage: true
+  - name: v3alpha1
+    schema:
+      openAPIV3Schema:
+        description: TLSContext is the Schema for the tlscontexts API
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: TLSContextSpec defines the desired state of TLSContext
+            properties:
+              alpn_protocols:
+                type: string
+              ambassador_id:
+                description: "AmbassadorID declares which Ambassador instances should
+                  pay attention to this resource. If no value is provided, the default
+                  is: \n ambassador_id: - \"default\""
+                items:
+                  type: string
+                type: array
+              ca_secret:
+                type: string
+              cacert_chain_file:
+                type: string
+              cert_chain_file:
+                type: string
+              cert_required:
+                type: boolean
+              cipher_suites:
+                items:
+                  type: string
+                type: array
+              crl_secret:
+                type: string
+              ecdh_curves:
+                items:
+                  type: string
+                type: array
+              hosts:
+                items:
+                  type: string
+                type: array
+              max_tls_version:
+                enum:
+                - v1.0
+                - v1.1
+                - v1.2
+                - v1.3
+                type: string
+              min_tls_version:
+                enum:
+                - v1.0
+                - v1.1
+                - v1.2
+                - v1.3
+                type: string
+              private_key_file:
+                type: string
+              redirect_cleartext_from:
+                type: integer
+              secret:
+                type: string
+              secret_namespacing:
+                type: boolean
+              sni:
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: false
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.12.0
+  labels:
+    app.kubernetes.io/instance: emissary-apiext
+    app.kubernetes.io/managed-by: kubectl_apply_-f_emissary-apiext.yaml
+    app.kubernetes.io/name: emissary-apiext
+    app.kubernetes.io/part-of: emissary-apiext
+  name: hosts.getambassador.io
+spec:
+  conversion:
+    strategy: Webhook
+    webhook:
+      clientConfig:
+        service:
+          name: emissary-apiext
+          namespace: emissary-system
+      conversionReviewVersions:
+      - v1
+  group: getambassador.io
+  names:
+    categories:
+    - ambassador-crds
+    kind: Host
+    listKind: HostList
+    plural: hosts
+    singular: host
+  preserveUnknownFields: false
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - jsonPath: .spec.hostname
+      name: Hostname
+      type: string
+    - jsonPath: .status.state
+      name: State
+      type: string
+    - jsonPath: .status.phaseCompleted
+      name: Phase Completed
+      type: string
+    - jsonPath: .status.phasePending
+      name: Phase Pending
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v2
+    schema:
+      openAPIV3Schema:
+        description: Host is the Schema for the hosts API
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: HostSpec defines the desired state of Host
+            properties:
+              acmeProvider:
+                description: Specifies whether/who to talk ACME with to automatically
+                  manage the $tlsSecret.
+                properties:
+                  authority:
+                    description: Specifies who to talk ACME with to get certs. Defaults
+                      to Let's Encrypt; if "none" (case-insensitive), do not try to
+                      do ACME for this Host.
+                    type: string
+                  email:
+                    type: string
+                  privateKeySecret:
+                    description: "Specifies the Kubernetes Secret to use to store
+                      the private key of the ACME account (essentially, where to store
+                      the auto-generated password for the auto-created ACME account).
+                      \ You should not normally need to set this--the default value
+                      is based on a combination of the ACME authority being registered
+                      wit and the email address associated with the account. \n Note
+                      that this is a native-Kubernetes-style core.v1.LocalObjectReference,
+                      not an Ambassador-style `{name}.{namespace}` string.  Because
+                      we're opinionated, it does not support referencing a Secret
+                      in another namespace (because most native Kubernetes resources
+                      don't support that), but if we ever abandon that opinion and
+                      decide to support non-local references it, it would be by adding
+                      a `namespace:` field by changing it from a core.v1.LocalObjectReference
+                      to a core.v1.SecretReference, not by adopting the `{name}.{namespace}`
+                      notation."
+                    properties:
+                      name:
+                        description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+                          TODO: Add other useful fields. apiVersion, kind, uid?'
+                        type: string
+                    type: object
+                    x-kubernetes-map-type: atomic
+                  registration:
+                    description: This is normally set automatically
+                    type: string
+                type: object
+              ambassador_id:
+                description: Common to all Ambassador objects (and optional).
+                items:
+                  type: string
+                type: array
+              hostname:
+                description: Hostname by which the Ambassador can be reached.
+                type: string
+              previewUrl:
+                description: Configuration for the Preview URL feature of Service
+                  Preview. Defaults to preview URLs not enabled.
+                properties:
+                  enabled:
+                    description: Is the Preview URL feature enabled?
+                    type: boolean
+                  type:
+                    description: What type of Preview URL is allowed?
+                    enum:
+                    - Path
+                    type: string
+                type: object
+              requestPolicy:
+                description: Request policy definition.
+                properties:
+                  insecure:
+                    properties:
+                      action:
+                        enum:
+                        - Redirect
+                        - Reject
+                        - Route
+                        type: string
+                      additionalPort:
+                        type: integer
+                    type: object
+                type: object
+              selector:
+                description: Selector by which we can find further configuration.
+                  Defaults to hostname=$hostname
+                properties:
+                  matchExpressions:
+                    description: matchExpressions is a list of label selector requirements.
+                      The requirements are ANDed.
+                    items:
+                      description: A label selector requirement is a selector that
+                        contains values, a key, and an operator that relates the key
+                        and values.
+                      properties:
+                        key:
+                          description: key is the label key that the selector applies
+                            to.
+                          type: string
+                        operator:
+                          description: operator represents a key's relationship to
+                            a set of values. Valid operators are In, NotIn, Exists
+                            and DoesNotExist.
+                          type: string
+                        values:
+                          description: values is an array of string values. If the
+                            operator is In or NotIn, the values array must be non-empty.
+                            If the operator is Exists or DoesNotExist, the values
+                            array must be empty. This array is replaced during a strategic
+                            merge patch.
+                          items:
+                            type: string
+                          type: array
+                      required:
+                      - key
+                      - operator
+                      type: object
+                    type: array
+                  matchLabels:
+                    additionalProperties:
+                      type: string
+                    description: matchLabels is a map of {key,value} pairs. A single
+                      {key,value} in the matchLabels map is equivalent to an element
+                      of matchExpressions, whose key field is "key", the operator
+                      is "In", and the values array contains only "value". The requirements
+                      are ANDed.
+                    type: object
+                type: object
+                x-kubernetes-map-type: atomic
+              tls:
+                description: TLS configuration.  It is not valid to specify both `tlsContext`
+                  and `tls`.
+                properties:
+                  alpn_protocols:
+                    type: string
+                  ca_secret:
+                    type: string
+                  cacert_chain_file:
+                    type: string
+                  cert_chain_file:
+                    type: string
+                  cert_required:
+                    type: boolean
+                  cipher_suites:
+                    items:
+                      type: string
+                    type: array
+                  ecdh_curves:
+                    items:
+                      type: string
+                    type: array
+                  max_tls_version:
+                    type: string
+                  min_tls_version:
+                    type: string
+                  private_key_file:
+                    type: string
+                  redirect_cleartext_from:
+                    type: integer
+                  sni:
+                    type: string
+                  v3CRLSecret:
+                    type: string
+                type: object
+              tlsContext:
+                description: "Name of the TLSContext the Host resource is linked with.
+                  It is not valid to specify both `tlsContext` and `tls`. \n Note
+                  that this is a native-Kubernetes-style core.v1.LocalObjectReference,
+                  not an Ambassador-style `{name}.{namespace}` string.  Because we're
+                  opinionated, it does not support referencing a Secret in another
+                  namespace (because most native Kubernetes resources don't support
+                  that), but if we ever abandon that opinion and decide to support
+                  non-local references it, it would be by adding a `namespace:` field
+                  by changing it from a core.v1.LocalObjectReference to a core.v1.SecretReference,
+                  not by adopting the `{name}.{namespace}` notation."
+                properties:
+                  name:
+                    description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+                      TODO: Add other useful fields. apiVersion, kind, uid?'
+                    type: string
+                type: object
+                x-kubernetes-map-type: atomic
+              tlsSecret:
+                description: Name of the Kubernetes secret into which to save generated
+                  certificates.  If ACME is enabled (see $acmeProvider), then the
+                  default is $hostname; otherwise the default is "".  If the value
+                  is "", then we do not do TLS for this Host.
+                properties:
+                  name:
+                    description: name is unique within a namespace to reference a
+                      secret resource.
+                    type: string
+                  namespace:
+                    description: namespace defines the space within which the secret
+                      name must be unique.
+                    type: string
+                type: object
+                x-kubernetes-map-type: atomic
+            type: object
+            x-kubernetes-preserve-unknown-fields: true
+          status:
+            description: HostStatus defines the observed state of Host
+            properties:
+              errorBackoff:
+                type: string
+              errorReason:
+                description: errorReason, errorTimestamp, and errorBackoff are valid
+                  when state==Error.
+                type: string
+              errorTimestamp:
+                format: date-time
+                type: string
+              phaseCompleted:
+                description: phaseCompleted and phasePending are valid when state==Pending
+                  or state==Error.
+                enum:
+                - NA
+                - DefaultsFilled
+                - ACMEUserPrivateKeyCreated
+                - ACMEUserRegistered
+                - ACMECertificateChallenge
+                type: string
+              phasePending:
+                description: phaseCompleted and phasePending are valid when state==Pending
+                  or state==Error.
+                enum:
+                - NA
+                - DefaultsFilled
+                - ACMEUserPrivateKeyCreated
+                - ACMEUserRegistered
+                - ACMECertificateChallenge
+                type: string
+              state:
+                enum:
+                - Initial
+                - Pending
+                - Ready
+                - Error
+                type: string
+              tlsCertificateSource:
+                enum:
+                - Unknown
+                - None
+                - Other
+                - ACME
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+  - additionalPrinterColumns:
+    - jsonPath: .spec.hostname
+      name: Hostname
+      type: string
+    - jsonPath: .status.state
+      name: State
+      type: string
+    - jsonPath: .status.phaseCompleted
+      name: Phase Completed
+      type: string
+    - jsonPath: .status.phasePending
+      name: Phase Pending
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v3alpha1
+    schema:
+      openAPIV3Schema:
+        description: Host is the Schema for the hosts API
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: HostSpec defines the desired state of Host
+            properties:
+              acmeProvider:
+                description: Specifies whether/who to talk ACME with to automatically
+                  manage the $tlsSecret.
+                properties:
+                  authority:
+                    description: Specifies who to talk ACME with to get certs. Defaults
+                      to Let's Encrypt; if "none" (case-insensitive), do not try to
+                      do ACME for this Host.
+                    type: string
+                  email:
+                    type: string
+                  privateKeySecret:
+                    description: "Specifies the Kubernetes Secret to use to store
+                      the private key of the ACME account (essentially, where to store
+                      the auto-generated password for the auto-created ACME account).
+                      \ You should not normally need to set this--the default value
+                      is based on a combination of the ACME authority being registered
+                      wit and the email address associated with the account. \n Note
+                      that this is a native-Kubernetes-style core.v1.LocalObjectReference,
+                      not an Ambassador-style `{name}.{namespace}` string.  Because
+                      we're opinionated, it does not support referencing a Secret
+                      in another namespace (because most native Kubernetes resources
+                      don't support that), but if we ever abandon that opinion and
+                      decide to support non-local references it, it would be by adding
+                      a `namespace:` field by changing it from a core.v1.LocalObjectReference
+                      to a core.v1.SecretReference, not by adopting the `{name}.{namespace}`
+                      notation."
+                    properties:
+                      name:
+                        description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+                          TODO: Add other useful fields. apiVersion, kind, uid?'
+                        type: string
+                    type: object
+                    x-kubernetes-map-type: atomic
+                  registration:
+                    description: This is normally set automatically
+                    type: string
+                type: object
+              ambassador_id:
+                description: Common to all Ambassador objects (and optional).
+                items:
+                  type: string
+                type: array
+              hostname:
+                description: Hostname by which the Ambassador can be reached.
+                type: string
+              mappingSelector:
+                description: Selector for Mappings we'll associate with this Host.
+                  At the moment, Selector and MappingSelector are synonyms, but that
+                  will change soon.
+                properties:
+                  matchExpressions:
+                    description: matchExpressions is a list of label selector requirements.
+                      The requirements are ANDed.
+                    items:
+                      description: A label selector requirement is a selector that
+                        contains values, a key, and an operator that relates the key
+                        and values.
+                      properties:
+                        key:
+                          description: key is the label key that the selector applies
+                            to.
+                          type: string
+                        operator:
+                          description: operator represents a key's relationship to
+                            a set of values. Valid operators are In, NotIn, Exists
+                            and DoesNotExist.
+                          type: string
+                        values:
+                          description: values is an array of string values. If the
+                            operator is In or NotIn, the values array must be non-empty.
+                            If the operator is Exists or DoesNotExist, the values
+                            array must be empty. This array is replaced during a strategic
+                            merge patch.
+                          items:
+                            type: string
+                          type: array
+                      required:
+                      - key
+                      - operator
+                      type: object
+                    type: array
+                  matchLabels:
+                    additionalProperties:
+                      type: string
+                    description: matchLabels is a map of {key,value} pairs. A single
+                      {key,value} in the matchLabels map is equivalent to an element
+                      of matchExpressions, whose key field is "key", the operator
+                      is "In", and the values array contains only "value". The requirements
+                      are ANDed.
+                    type: object
+                type: object
+                x-kubernetes-map-type: atomic
+              previewUrl:
+                description: Configuration for the Preview URL feature of Service
+                  Preview. Defaults to preview URLs not enabled.
+                properties:
+                  enabled:
+                    description: Is the Preview URL feature enabled?
+                    type: boolean
+                  type:
+                    description: What type of Preview URL is allowed?
+                    enum:
+                    - Path
+                    type: string
+                type: object
+              requestPolicy:
+                description: Request policy definition.
+                properties:
+                  insecure:
+                    properties:
+                      action:
+                        enum:
+                        - Redirect
+                        - Reject
+                        - Route
+                        type: string
+                      additionalPort:
+                        type: integer
+                    type: object
+                type: object
+              selector:
+                description: 'DEPRECATED: Selector by which we can find further configuration.
+                  Use MappingSelector instead.'
+                properties:
+                  matchExpressions:
+                    description: matchExpressions is a list of label selector requirements.
+                      The requirements are ANDed.
+                    items:
+                      description: A label selector requirement is a selector that
+                        contains values, a key, and an operator that relates the key
+                        and values.
+                      properties:
+                        key:
+                          description: key is the label key that the selector applies
+                            to.
+                          type: string
+                        operator:
+                          description: operator represents a key's relationship to
+                            a set of values. Valid operators are In, NotIn, Exists
+                            and DoesNotExist.
+                          type: string
+                        values:
+                          description: values is an array of string values. If the
+                            operator is In or NotIn, the values array must be non-empty.
+                            If the operator is Exists or DoesNotExist, the values
+                            array must be empty. This array is replaced during a strategic
+                            merge patch.
+                          items:
+                            type: string
+                          type: array
+                      required:
+                      - key
+                      - operator
+                      type: object
+                    type: array
+                  matchLabels:
+                    additionalProperties:
+                      type: string
+                    description: matchLabels is a map of {key,value} pairs. A single
+                      {key,value} in the matchLabels map is equivalent to an element
+                      of matchExpressions, whose key field is "key", the operator
+                      is "In", and the values array contains only "value". The requirements
+                      are ANDed.
+                    type: object
+                type: object
+                x-kubernetes-map-type: atomic
+              tls:
+                description: TLS configuration.  It is not valid to specify both `tlsContext`
+                  and `tls`.
+                properties:
+                  alpn_protocols:
+                    type: string
+                  ca_secret:
+                    type: string
+                  cacert_chain_file:
+                    type: string
+                  cert_chain_file:
+                    type: string
+                  cert_required:
+                    type: boolean
+                  cipher_suites:
+                    items:
+                      type: string
+                    type: array
+                  crl_secret:
+                    type: string
+                  ecdh_curves:
+                    items:
+                      type: string
+                    type: array
+                  max_tls_version:
+                    type: string
+                  min_tls_version:
+                    type: string
+                  private_key_file:
+                    type: string
+                  redirect_cleartext_from:
+                    type: integer
+                  sni:
+                    type: string
+                type: object
+              tlsContext:
+                description: "Name of the TLSContext the Host resource is linked with.
+                  It is not valid to specify both `tlsContext` and `tls`. \n Note
+                  that this is a native-Kubernetes-style core.v1.LocalObjectReference,
+                  not an Ambassador-style `{name}.{namespace}` string.  Because we're
+                  opinionated, it does not support referencing a Secret in another
+                  namespace (because most native Kubernetes resources don't support
+                  that), but if we ever abandon that opinion and decide to support
+                  non-local references it, it would be by adding a `namespace:` field
+                  by changing it from a core.v1.LocalObjectReference to a core.v1.SecretReference,
+                  not by adopting the `{name}.{namespace}` notation."
+                properties:
+                  name:
+                    description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+                      TODO: Add other useful fields. apiVersion, kind, uid?'
+                    type: string
+                type: object
+                x-kubernetes-map-type: atomic
+              tlsSecret:
+                description: Name of the Kubernetes secret into which to save generated
+                  certificates.  If ACME is enabled (see $acmeProvider), then the
+                  default is $hostname; otherwise the default is "".  If the value
+                  is "", then we do not do TLS for this Host.
+                properties:
+                  name:
+                    description: name is unique within a namespace to reference a
+                      secret resource.
+                    type: string
+                  namespace:
+                    description: namespace defines the space within which the secret
+                      name must be unique.
+                    type: string
+                type: object
+                x-kubernetes-map-type: atomic
+            type: object
+          status:
+            description: HostStatus defines the observed state of Host
+            properties:
+              errorBackoff:
+                type: string
+              errorReason:
+                description: errorReason, errorTimestamp, and errorBackoff are valid
+                  when state==Error.
+                type: string
+              errorTimestamp:
+                format: date-time
+                type: string
+              phaseCompleted:
+                description: phaseCompleted and phasePending are valid when state==Pending
+                  or state==Error.
+                enum:
+                - NA
+                - DefaultsFilled
+                - ACMEUserPrivateKeyCreated
+                - ACMEUserRegistered
+                - ACMECertificateChallenge
+                type: string
+              phasePending:
+                description: phaseCompleted and phasePending are valid when state==Pending
+                  or state==Error.
+                enum:
+                - NA
+                - DefaultsFilled
+                - ACMEUserPrivateKeyCreated
+                - ACMEUserRegistered
+                - ACMECertificateChallenge
+                type: string
+              state:
+                enum:
+                - Initial
+                - Pending
+                - Ready
+                - Error
+                type: string
+              tlsCertificateSource:
+                enum:
+                - Unknown
+                - None
+                - Other
+                - ACME
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: false
+    subresources:
+      status: {}
diff --git a/charts/k8s-gerrit/operator/src/main/resources/log4j2.xml b/charts/k8s-gerrit/operator/src/main/resources/log4j2.xml
new file mode 100644
index 0000000..f3dd273
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/resources/log4j2.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration status="INFO">
+    <Appenders>
+        <Console name="Console" target="SYSTEM_OUT">
+            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%-5level] %c:%L [PID:%pid] - %msg%n"/>
+        </Console>
+    </Appenders>
+    <Loggers>
+        <Root level="info">
+            <AppenderRef ref="Console"/>
+        </Root>
+    </Loggers>
+</Configuration>
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/cluster/GerritClusterE2E.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/cluster/GerritClusterE2E.java
new file mode 100644
index 0000000..b73f463
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/cluster/GerritClusterE2E.java
@@ -0,0 +1,74 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.cluster;
+
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.notNullValue;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.k8s.operator.cluster.dependent.NfsIdmapdConfigMap;
+import com.google.gerrit.k8s.operator.cluster.dependent.SharedPVC;
+import com.google.gerrit.k8s.operator.network.IngressType;
+import com.google.gerrit.k8s.operator.test.AbstractGerritOperatorE2ETest;
+import io.fabric8.kubernetes.api.model.ConfigMap;
+import io.fabric8.kubernetes.api.model.PersistentVolumeClaim;
+import org.junit.jupiter.api.Test;
+
+public class GerritClusterE2E extends AbstractGerritOperatorE2ETest {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  @Test
+  void testSharedPvcCreated() {
+    logger.atInfo().log("Waiting max 1 minutes for the shared pvc to be created.");
+    await()
+        .atMost(1, MINUTES)
+        .untilAsserted(
+            () -> {
+              PersistentVolumeClaim pvc =
+                  client
+                      .persistentVolumeClaims()
+                      .inNamespace(operator.getNamespace())
+                      .withName(SharedPVC.SHARED_PVC_NAME)
+                      .get();
+              assertThat(pvc, is(notNullValue()));
+            });
+  }
+
+  @Test
+  void testNfsIdmapdConfigMapCreated() {
+    gerritCluster.setNfsEnabled(true);
+    logger.atInfo().log("Waiting max 1 minutes for the nfs idmapd configmap to be created.");
+    await()
+        .atMost(1, MINUTES)
+        .untilAsserted(
+            () -> {
+              ConfigMap cm =
+                  client
+                      .configMaps()
+                      .inNamespace(operator.getNamespace())
+                      .withName(NfsIdmapdConfigMap.NFS_IDMAPD_CM_NAME)
+                      .get();
+              assertThat(cm, is(notNullValue()));
+            });
+  }
+
+  @Override
+  protected IngressType getIngressType() {
+    return IngressType.INGRESS;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/cluster/GerritRepositoryConfigTest.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/cluster/GerritRepositoryConfigTest.java
new file mode 100644
index 0000000..5474780
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/cluster/GerritRepositoryConfigTest.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.cluster;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GerritRepositoryConfig;
+import org.junit.jupiter.api.Test;
+
+public class GerritRepositoryConfigTest {
+
+  @Test
+  public void testFullImageNameComputesCorrectly() {
+    assertThat(
+        new GerritRepositoryConfig().getFullImageName("gerrit"),
+        is(equalTo("docker.io/k8sgerrit/gerrit:latest")));
+
+    GerritRepositoryConfig repoConfig1 = new GerritRepositoryConfig();
+    repoConfig1.setOrg("testorg");
+    repoConfig1.setRegistry("registry.example.com");
+    repoConfig1.setTag("v1.0");
+    assertThat(
+        repoConfig1.getFullImageName("gerrit"),
+        is(equalTo("registry.example.com/testorg/gerrit:v1.0")));
+
+    GerritRepositoryConfig repoConfig2 = new GerritRepositoryConfig();
+    repoConfig2.setOrg(null);
+    repoConfig2.setRegistry(null);
+    repoConfig2.setTag(null);
+    assertThat(repoConfig2.getFullImageName("gerrit"), is(equalTo("gerrit")));
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/ClusterManagedGerritWithIngressE2E.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/ClusterManagedGerritWithIngressE2E.java
new file mode 100644
index 0000000..c848aa9
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/ClusterManagedGerritWithIngressE2E.java
@@ -0,0 +1,140 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gerrit;
+
+import static com.google.gerrit.k8s.operator.network.ingress.dependent.GerritClusterIngress.INGRESS_NAME;
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.k8s.operator.network.IngressType;
+import com.google.gerrit.k8s.operator.test.AbstractGerritOperatorE2ETest;
+import com.google.gerrit.k8s.operator.test.TestGerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplate;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplateSpec.GerritMode;
+import io.fabric8.kubernetes.api.model.networking.v1.Ingress;
+import io.fabric8.kubernetes.api.model.networking.v1.IngressLoadBalancerIngress;
+import io.fabric8.kubernetes.api.model.networking.v1.IngressStatus;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+public class ClusterManagedGerritWithIngressE2E extends AbstractGerritOperatorE2ETest {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  @Test
+  void testPrimaryGerritIsCreated() throws Exception {
+    TestGerrit gerrit =
+        new TestGerrit(client, testProps, GerritMode.PRIMARY, "gerrit", operator.getNamespace());
+    GerritTemplate gerritTemplate = gerrit.createGerritTemplate();
+    gerritCluster.addGerrit(gerritTemplate);
+    gerritCluster.deploy();
+
+    logger.atInfo().log("Waiting max 2 minutes for the Ingress to have an external IP.");
+    await()
+        .atMost(2, MINUTES)
+        .untilAsserted(
+            () -> {
+              Ingress ingress =
+                  client
+                      .network()
+                      .v1()
+                      .ingresses()
+                      .inNamespace(operator.getNamespace())
+                      .withName(INGRESS_NAME)
+                      .get();
+              assertThat(ingress, is(notNullValue()));
+              IngressStatus status = ingress.getStatus();
+              assertThat(status, is(notNullValue()));
+              List<IngressLoadBalancerIngress> lbIngresses = status.getLoadBalancer().getIngress();
+              assertThat(lbIngresses, hasSize(1));
+              assertThat(lbIngresses.get(0).getIp(), is(notNullValue()));
+            });
+
+    GerritApi gerritApi = gerritCluster.getGerritApiClient(gerritTemplate, IngressType.INGRESS);
+    await()
+        .atMost(2, MINUTES)
+        .untilAsserted(
+            () -> {
+              assertDoesNotThrow(() -> gerritApi.config().server().getVersion());
+              assertThat(gerritApi.config().server().getVersion(), notNullValue());
+              assertThat(gerritApi.config().server().getVersion(), not(is("<2.8")));
+              logger.atInfo().log("Gerrit version: %s", gerritApi.config().server().getVersion());
+            });
+  }
+
+  @Test
+  void testGerritReplicaIsCreated() throws Exception {
+    String gerritName = "gerrit-replica";
+    TestGerrit gerrit =
+        new TestGerrit(client, testProps, GerritMode.REPLICA, gerritName, operator.getNamespace());
+    gerritCluster.addGerrit(gerrit.createGerritTemplate());
+    gerritCluster.deploy();
+
+    assertTrue(
+        client
+            .pods()
+            .inNamespace(operator.getNamespace())
+            .withName(gerritName + "-0")
+            .inContainer("gerrit")
+            .getLog()
+            .contains("Gerrit Code Review [replica]"));
+  }
+
+  @Test
+  void testGerritReplicaAndPrimaryGerritAreCreated() throws Exception {
+    String primaryGerritName = "gerrit";
+    TestGerrit primaryGerrit =
+        new TestGerrit(
+            client, testProps, GerritMode.PRIMARY, primaryGerritName, operator.getNamespace());
+    gerritCluster.addGerrit(primaryGerrit.createGerritTemplate());
+    String gerritReplicaName = "gerrit-replica";
+    TestGerrit gerritReplica =
+        new TestGerrit(
+            client, testProps, GerritMode.REPLICA, gerritReplicaName, operator.getNamespace());
+    gerritCluster.addGerrit(gerritReplica.createGerritTemplate());
+    gerritCluster.deploy();
+
+    assertTrue(
+        client
+            .pods()
+            .inNamespace(operator.getNamespace())
+            .withName(primaryGerritName + "-0")
+            .inContainer("gerrit")
+            .getLog()
+            .contains("Gerrit Code Review"));
+
+    assertTrue(
+        client
+            .pods()
+            .inNamespace(operator.getNamespace())
+            .withName(gerritReplicaName + "-0")
+            .inContainer("gerrit")
+            .getLog()
+            .contains("Gerrit Code Review [replica]"));
+  }
+
+  @Override
+  protected IngressType getIngressType() {
+    return IngressType.INGRESS;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/ClusterManagedGerritWithIstioE2E.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/ClusterManagedGerritWithIstioE2E.java
new file mode 100644
index 0000000..fdf5ccb
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/ClusterManagedGerritWithIstioE2E.java
@@ -0,0 +1,144 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gerrit;
+
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.k8s.operator.network.IngressType;
+import com.google.gerrit.k8s.operator.test.AbstractGerritOperatorE2ETest;
+import com.google.gerrit.k8s.operator.test.TestGerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplate;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplateSpec.GerritMode;
+import org.junit.jupiter.api.Test;
+
+public class ClusterManagedGerritWithIstioE2E extends AbstractGerritOperatorE2ETest {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  @Test
+  void testPrimaryGerritWithIstio() throws Exception {
+    GerritTemplate gerrit =
+        new TestGerrit(client, testProps, GerritMode.PRIMARY, "gerrit", operator.getNamespace())
+            .createGerritTemplate();
+    gerritCluster.addGerrit(gerrit);
+    gerritCluster.deploy();
+
+    GerritApi gerritApi = gerritCluster.getGerritApiClient(gerrit, IngressType.ISTIO);
+    await()
+        .atMost(2, MINUTES)
+        .untilAsserted(
+            () -> {
+              assertDoesNotThrow(() -> gerritApi.config().server().getVersion());
+              assertThat(gerritApi.config().server().getVersion(), notNullValue());
+              assertThat(gerritApi.config().server().getVersion(), not(is("<2.8")));
+              logger.atInfo().log("Gerrit version: %s", gerritApi.config().server().getVersion());
+            });
+  }
+
+  @Test
+  void testGerritReplicaIsCreated() throws Exception {
+    String gerritName = "gerrit-replica";
+    TestGerrit gerrit =
+        new TestGerrit(client, testProps, GerritMode.REPLICA, gerritName, operator.getNamespace());
+    gerritCluster.addGerrit(gerrit.createGerritTemplate());
+    gerritCluster.deploy();
+
+    assertTrue(
+        client
+            .pods()
+            .inNamespace(operator.getNamespace())
+            .withName(gerritName + "-0")
+            .inContainer("gerrit")
+            .getLog()
+            .contains("Gerrit Code Review [replica]"));
+  }
+
+  @Test
+  void testMultipleGerritReplicaAreCreated() throws Exception {
+    String gerritName = "gerrit-replica-1";
+    TestGerrit gerrit =
+        new TestGerrit(client, testProps, GerritMode.REPLICA, gerritName, operator.getNamespace());
+    gerritCluster.addGerrit(gerrit.createGerritTemplate());
+    String gerritName2 = "gerrit-replica-2";
+    TestGerrit gerrit2 =
+        new TestGerrit(client, testProps, GerritMode.REPLICA, gerritName2, operator.getNamespace());
+    gerritCluster.addGerrit(gerrit2.createGerritTemplate());
+    gerritCluster.deploy();
+
+    assertTrue(
+        client
+            .pods()
+            .inNamespace(operator.getNamespace())
+            .withName(gerritName + "-0")
+            .inContainer("gerrit")
+            .getLog()
+            .contains("Gerrit Code Review [replica]"));
+
+    assertTrue(
+        client
+            .pods()
+            .inNamespace(operator.getNamespace())
+            .withName(gerritName2 + "-0")
+            .inContainer("gerrit")
+            .getLog()
+            .contains("Gerrit Code Review [replica]"));
+  }
+
+  @Test
+  void testGerritReplicaAndPrimaryGerritAreCreated() throws Exception {
+    String primaryGerritName = "gerrit";
+    TestGerrit primaryGerrit =
+        new TestGerrit(
+            client, testProps, GerritMode.PRIMARY, primaryGerritName, operator.getNamespace());
+    gerritCluster.addGerrit(primaryGerrit.createGerritTemplate());
+    String gerritReplicaName = "gerrit-replica";
+    TestGerrit gerritReplica =
+        new TestGerrit(
+            client, testProps, GerritMode.REPLICA, gerritReplicaName, operator.getNamespace());
+    gerritCluster.addGerrit(gerritReplica.createGerritTemplate());
+    gerritCluster.deploy();
+
+    assertTrue(
+        client
+            .pods()
+            .inNamespace(operator.getNamespace())
+            .withName(primaryGerritName + "-0")
+            .inContainer("gerrit")
+            .getLog()
+            .contains("Gerrit Code Review"));
+
+    assertTrue(
+        client
+            .pods()
+            .inNamespace(operator.getNamespace())
+            .withName(gerritReplicaName + "-0")
+            .inContainer("gerrit")
+            .getLog()
+            .contains("Gerrit Code Review [replica]"));
+  }
+
+  @Override
+  protected IngressType getIngressType() {
+    return IngressType.ISTIO;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/GerritConfigReconciliationE2E.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/GerritConfigReconciliationE2E.java
new file mode 100644
index 0000000..342d524
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/GerritConfigReconciliationE2E.java
@@ -0,0 +1,163 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gerrit;
+
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.gerrit.k8s.operator.network.IngressType;
+import com.google.gerrit.k8s.operator.test.AbstractGerritOperatorE2ETest;
+import com.google.gerrit.k8s.operator.test.TestGerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplate;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplateSpec;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplateSpec.GerritMode;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.HttpSshServiceConfig;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class GerritConfigReconciliationE2E extends AbstractGerritOperatorE2ETest {
+  private static final String GERRIT_NAME = "gerrit";
+  private static final String RESTART_ANNOTATION = "kubectl.kubernetes.io/restartedAt";
+
+  private GerritTemplate gerritTemplate;
+
+  @BeforeEach
+  public void setupGerrit() throws Exception {
+    TestGerrit gerrit =
+        new TestGerrit(client, testProps, GerritMode.PRIMARY, GERRIT_NAME, operator.getNamespace());
+    gerritTemplate = gerrit.createGerritTemplate();
+    gerritCluster.addGerrit(gerritTemplate);
+    gerritCluster.deploy();
+  }
+
+  @Test
+  void testNoRestartIfGerritConfigUnchanged() throws Exception {
+    Map<String, String> annotations = getReplicaSetAnnotations();
+    assertFalse(annotations.containsKey(RESTART_ANNOTATION));
+
+    gerritCluster.removeGerrit(gerritTemplate);
+    GerritTemplateSpec gerritSpec = gerritTemplate.getSpec();
+    HttpSshServiceConfig gerritServiceConfig = new HttpSshServiceConfig();
+    gerritServiceConfig.setHttpPort(48080);
+    gerritSpec.setService(gerritServiceConfig);
+    gerritTemplate.setSpec(gerritSpec);
+    gerritCluster.addGerrit(gerritTemplate);
+    gerritCluster.deploy();
+
+    await()
+        .atMost(30, SECONDS)
+        .untilAsserted(
+            () -> {
+              assertTrue(
+                  client
+                      .services()
+                      .inNamespace(operator.getNamespace())
+                      .withName(GERRIT_NAME)
+                      .get()
+                      .getSpec()
+                      .getPorts()
+                      .stream()
+                      .allMatch(p -> p.getPort() == 48080));
+              assertFalse(getReplicaSetAnnotations().containsKey(RESTART_ANNOTATION));
+            });
+  }
+
+  @Test
+  void testRestartOnGerritConfigMapChange() throws Exception {
+    String podV1Uid =
+        client
+            .pods()
+            .inNamespace(operator.getNamespace())
+            .withName(GERRIT_NAME + "-0")
+            .get()
+            .getMetadata()
+            .getUid();
+
+    gerritCluster.removeGerrit(gerritTemplate);
+    GerritTemplateSpec gerritSpec = gerritTemplate.getSpec();
+    Map<String, String> cfgs = new HashMap<>();
+    cfgs.putAll(gerritSpec.getConfigFiles());
+    cfgs.put("test.config", "[test]\n  test");
+    gerritSpec.setConfigFiles(cfgs);
+    gerritTemplate.setSpec(gerritSpec);
+    gerritCluster.addGerrit(gerritTemplate);
+    gerritCluster.deploy();
+
+    assertGerritRestart(podV1Uid);
+  }
+
+  @Test
+  void testRestartOnGerritSecretChange() throws Exception {
+    String podV1Uid =
+        client
+            .pods()
+            .inNamespace(operator.getNamespace())
+            .withName(GERRIT_NAME + "-0")
+            .get()
+            .getMetadata()
+            .getUid();
+
+    secureConfig.modify("test", "test", "test");
+
+    assertGerritRestart(podV1Uid);
+  }
+
+  private void assertGerritRestart(String uidOld) {
+    await()
+        .atMost(2, MINUTES)
+        .untilAsserted(
+            () -> {
+              assertTrue(
+                  client
+                      .pods()
+                      .inNamespace(operator.getNamespace())
+                      .withName(GERRIT_NAME + "-0")
+                      .isReady());
+              assertTrue(getReplicaSetAnnotations().containsKey(RESTART_ANNOTATION));
+              assertFalse(
+                  uidOld.equals(
+                      client
+                          .pods()
+                          .inNamespace(operator.getNamespace())
+                          .withName(GERRIT_NAME + "-0")
+                          .get()
+                          .getMetadata()
+                          .getUid()));
+            });
+  }
+
+  private Map<String, String> getReplicaSetAnnotations() {
+    return client
+        .apps()
+        .statefulSets()
+        .inNamespace(operator.getNamespace())
+        .withName(GERRIT_NAME)
+        .get()
+        .getSpec()
+        .getTemplate()
+        .getMetadata()
+        .getAnnotations();
+  }
+
+  @Override
+  protected IngressType getIngressType() {
+    return IngressType.INGRESS;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/StandaloneGerritE2E.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/StandaloneGerritE2E.java
new file mode 100644
index 0000000..b7359ab
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/StandaloneGerritE2E.java
@@ -0,0 +1,64 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gerrit;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.gerrit.k8s.operator.network.IngressType;
+import com.google.gerrit.k8s.operator.test.AbstractGerritOperatorE2ETest;
+import com.google.gerrit.k8s.operator.test.TestGerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplateSpec.GerritMode;
+import org.junit.jupiter.api.Test;
+
+public class StandaloneGerritE2E extends AbstractGerritOperatorE2ETest {
+
+  @Test
+  void testPrimaryGerritIsCreated() throws Exception {
+    String gerritName = "gerrit";
+    TestGerrit testGerrit = new TestGerrit(client, testProps, gerritName, operator.getNamespace());
+    testGerrit.deploy();
+
+    assertTrue(
+        client
+            .pods()
+            .inNamespace(operator.getNamespace())
+            .withName(gerritName + "-0")
+            .inContainer("gerrit")
+            .getLog()
+            .contains("Gerrit Code Review"));
+  }
+
+  @Test
+  void testGerritReplicaIsCreated() throws Exception {
+    String gerritName = "gerrit-replica";
+    TestGerrit testGerrit =
+        new TestGerrit(client, testProps, GerritMode.REPLICA, gerritName, operator.getNamespace());
+    testGerrit.deploy();
+
+    assertTrue(
+        client
+            .pods()
+            .inNamespace(operator.getNamespace())
+            .withName(gerritName + "-0")
+            .inContainer("gerrit")
+            .getLog()
+            .contains("Gerrit Code Review [replica]"));
+  }
+
+  @Override
+  protected IngressType getIngressType() {
+    return IngressType.INGRESS;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/config/GerritConfigBuilderTest.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/config/GerritConfigBuilderTest.java
new file mode 100644
index 0000000..cedcebb
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/config/GerritConfigBuilderTest.java
@@ -0,0 +1,85 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gerrit.config;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritSpec;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.IngressConfig;
+import com.google.gerrit.k8s.operator.v1alpha.gerrit.config.GerritConfigBuilder;
+import java.util.Map;
+import java.util.Set;
+import org.assertj.core.util.Arrays;
+import org.eclipse.jgit.lib.Config;
+import org.junit.jupiter.api.Test;
+
+public class GerritConfigBuilderTest {
+
+  @Test
+  public void emptyGerritConfigContainsAllPresetConfiguration() {
+    Gerrit gerrit = createGerrit("");
+    ConfigBuilder cfgBuilder = new GerritConfigBuilder(gerrit);
+    Config cfg = cfgBuilder.build();
+    for (RequiredOption<?> opt : cfgBuilder.getRequiredOptions()) {
+      if (opt.getExpected() instanceof String || opt.getExpected() instanceof Boolean) {
+        assertTrue(
+            cfg.getString(opt.getSection(), opt.getSubSection(), opt.getKey())
+                .equals(opt.getExpected().toString()));
+      } else if (opt.getExpected() instanceof Set) {
+        assertTrue(
+            Arrays.asList(cfg.getStringList(opt.getSection(), opt.getSubSection(), opt.getKey()))
+                .containsAll((Set<?>) opt.getExpected()));
+      }
+    }
+  }
+
+  @Test
+  public void invalidConfigValueIsRejected() {
+    Gerrit gerrit = createGerrit("[gerrit]\n  basePath = invalid");
+    assertThrows(IllegalStateException.class, () -> new GerritConfigBuilder(gerrit).build());
+  }
+
+  @Test
+  public void validConfigValueIsAccepted() {
+    Gerrit gerrit = createGerrit("[gerrit]\n  basePath = git");
+    assertDoesNotThrow(() -> new GerritConfigBuilder(gerrit).build());
+  }
+
+  @Test
+  public void canonicalWebUrlIsConfigured() {
+    IngressConfig ingressConfig = new IngressConfig();
+    ingressConfig.setEnabled(true);
+    ingressConfig.setHost("gerrit.example.com");
+
+    GerritSpec gerritSpec = new GerritSpec();
+    gerritSpec.setIngress(ingressConfig);
+    Gerrit gerrit = new Gerrit();
+    gerrit.setSpec(gerritSpec);
+    Config cfg = new GerritConfigBuilder(gerrit).build();
+    assertTrue(
+        cfg.getString("gerrit", null, "canonicalWebUrl").equals("http://gerrit.example.com"));
+  }
+
+  private Gerrit createGerrit(String configText) {
+    GerritSpec gerritSpec = new GerritSpec();
+    gerritSpec.setConfigFiles(Map.of("gerrit.config", configText));
+    Gerrit gerrit = new Gerrit();
+    gerrit.setSpec(gerritSpec);
+    return gerrit;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollectionE2E.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollectionE2E.java
new file mode 100644
index 0000000..7f31ac2
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollectionE2E.java
@@ -0,0 +1,234 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gitgc;
+
+import static com.google.gerrit.k8s.operator.test.TestGerritCluster.CLUSTER_NAME;
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.k8s.operator.network.IngressType;
+import com.google.gerrit.k8s.operator.test.AbstractGerritOperatorE2ETest;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gitgc.GitGarbageCollection;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gitgc.GitGarbageCollectionSpec;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gitgc.GitGarbageCollectionStatus;
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+import io.fabric8.kubernetes.api.model.batch.v1.CronJob;
+import io.fabric8.kubernetes.api.model.batch.v1.Job;
+import java.util.List;
+import java.util.Set;
+import org.junit.jupiter.api.Test;
+
+public class GitGarbageCollectionE2E extends AbstractGerritOperatorE2ETest {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  static final String GITGC_SCHEDULE = "*/1 * * * *";
+
+  @Test
+  void testGitGcAllProjectsCreationAndDeletion() {
+    GitGarbageCollection gitGc = createCompleteGc();
+
+    logger.atInfo().log("Waiting max 2 minutes for GitGc to be created.");
+    await()
+        .atMost(2, MINUTES)
+        .untilAsserted(
+            () -> {
+              assertGitGcCreation(gitGc.getMetadata().getName());
+              assertGitGcCronJobCreation(gitGc.getMetadata().getName());
+              assertGitGcJobCreation(gitGc.getMetadata().getName());
+            });
+
+    logger.atInfo().log("Deleting test GitGc object: %s", gitGc);
+    client.resource(gitGc).delete();
+    awaitGitGcDeletionAssertion(gitGc.getMetadata().getName());
+  }
+
+  @Test
+  void testGitGcSelectedProjects() {
+    GitGarbageCollection gitGc = createSelectiveGc("selective-gc", Set.of("All-Projects", "test"));
+
+    logger.atInfo().log("Waiting max 2 minutes for GitGc to be created.");
+    await()
+        .atMost(2, MINUTES)
+        .untilAsserted(
+            () -> {
+              assertGitGcCreation(gitGc.getMetadata().getName());
+              assertGitGcCronJobCreation(gitGc.getMetadata().getName());
+              assertGitGcJobCreation(gitGc.getMetadata().getName());
+            });
+
+    client.resource(gitGc).delete();
+  }
+
+  @Test
+  void testSelectiveGcIsExcludedFromCompleteGc() {
+    GitGarbageCollection completeGitGc = createCompleteGc();
+
+    logger.atInfo().log("Waiting max 2 minutes for GitGc to be created.");
+    await()
+        .atMost(2, MINUTES)
+        .untilAsserted(
+            () -> {
+              assertGitGcCreation(completeGitGc.getMetadata().getName());
+              assertGitGcCronJobCreation(completeGitGc.getMetadata().getName());
+            });
+
+    Set<String> selectedProjects = Set.of("All-Projects", "test");
+    GitGarbageCollection selectiveGitGc = createSelectiveGc("selective-gc", selectedProjects);
+
+    logger.atInfo().log("Waiting max 2 minutes for GitGc to be created.");
+    await()
+        .atMost(2, MINUTES)
+        .untilAsserted(
+            () -> {
+              assertGitGcCreation(selectiveGitGc.getMetadata().getName());
+              assertGitGcCronJobCreation(selectiveGitGc.getMetadata().getName());
+            });
+
+    await()
+        .atMost(2, MINUTES)
+        .untilAsserted(
+            () -> {
+              GitGarbageCollection updatedCompleteGitGc =
+                  client
+                      .resources(GitGarbageCollection.class)
+                      .inNamespace(operator.getNamespace())
+                      .withName(completeGitGc.getMetadata().getName())
+                      .get();
+              assert updatedCompleteGitGc
+                  .getStatus()
+                  .getExcludedProjects()
+                  .containsAll(selectedProjects);
+            });
+
+    client.resource(selectiveGitGc).delete();
+    awaitGitGcDeletionAssertion(selectiveGitGc.getMetadata().getName());
+
+    await()
+        .atMost(2, MINUTES)
+        .untilAsserted(
+            () -> {
+              GitGarbageCollection updatedCompleteGitGc =
+                  client
+                      .resources(GitGarbageCollection.class)
+                      .inNamespace(operator.getNamespace())
+                      .withName(completeGitGc.getMetadata().getName())
+                      .get();
+              assert updatedCompleteGitGc.getStatus().getExcludedProjects().isEmpty();
+            });
+  }
+
+  private GitGarbageCollection createCompleteGc() {
+    GitGarbageCollection gitGc = new GitGarbageCollection();
+    gitGc.setMetadata(
+        new ObjectMetaBuilder()
+            .withName("gitgc-complete")
+            .withNamespace(operator.getNamespace())
+            .build());
+    GitGarbageCollectionSpec spec = new GitGarbageCollectionSpec();
+    spec.setSchedule(GITGC_SCHEDULE);
+    spec.setCluster(CLUSTER_NAME);
+    gitGc.setSpec(spec);
+
+    logger.atInfo().log("Creating test GitGc object: %s", gitGc);
+    client.resource(gitGc).createOrReplace();
+
+    return gitGc;
+  }
+
+  private GitGarbageCollection createSelectiveGc(String name, Set<String> projects) {
+    GitGarbageCollection gitGc = new GitGarbageCollection();
+    gitGc.setMetadata(
+        new ObjectMetaBuilder().withName(name).withNamespace(operator.getNamespace()).build());
+    GitGarbageCollectionSpec spec = new GitGarbageCollectionSpec();
+    spec.setSchedule(GITGC_SCHEDULE);
+    spec.setCluster(CLUSTER_NAME);
+    spec.setProjects(projects);
+    gitGc.setSpec(spec);
+
+    logger.atInfo().log("Creating test GitGc object: %s", gitGc);
+    client.resource(gitGc).createOrReplace();
+
+    return gitGc;
+  }
+
+  private void assertGitGcCreation(String gitGcName) {
+    GitGarbageCollection updatedGitGc =
+        client
+            .resources(GitGarbageCollection.class)
+            .inNamespace(operator.getNamespace())
+            .withName(gitGcName)
+            .get();
+    assertThat(updatedGitGc, is(notNullValue()));
+    assertThat(
+        updatedGitGc.getStatus().getState(),
+        is(not(equalTo(GitGarbageCollectionStatus.GitGcState.ERROR))));
+  }
+
+  private void assertGitGcCronJobCreation(String gitGcName) {
+    CronJob cronJob =
+        client
+            .batch()
+            .v1()
+            .cronjobs()
+            .inNamespace(operator.getNamespace())
+            .withName(gitGcName)
+            .get();
+    assertThat(cronJob, is(notNullValue()));
+  }
+
+  private void awaitGitGcDeletionAssertion(String gitGcName) {
+    logger.atInfo().log("Waiting max 2 minutes for GitGc to be deleted.");
+    await()
+        .atMost(2, MINUTES)
+        .untilAsserted(
+            () -> {
+              GitGarbageCollection updatedGitGc =
+                  client
+                      .resources(GitGarbageCollection.class)
+                      .inNamespace(operator.getNamespace())
+                      .withName(gitGcName)
+                      .get();
+              assertNull(updatedGitGc);
+
+              CronJob cronJob =
+                  client
+                      .batch()
+                      .v1()
+                      .cronjobs()
+                      .inNamespace(operator.getNamespace())
+                      .withName(gitGcName)
+                      .get();
+              assertNull(cronJob);
+            });
+  }
+
+  private void assertGitGcJobCreation(String gitGcName) {
+    List<Job> jobRuns =
+        client.batch().v1().jobs().inNamespace(operator.getNamespace()).list().getItems();
+    assert (jobRuns.size() > 0);
+    assert (jobRuns.get(0).getMetadata().getName().startsWith(gitGcName));
+  }
+
+  @Override
+  protected IngressType getIngressType() {
+    return IngressType.NONE;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterAmbassadorTest.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterAmbassadorTest.java
new file mode 100644
index 0000000..3f5a9cd
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterAmbassadorTest.java
@@ -0,0 +1,130 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Host;
+import io.getambassador.v2.Mapping;
+import io.getambassador.v2.TLSContext;
+import io.javaoperatorsdk.operator.ReconcilerUtils;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Map;
+import java.util.stream.Stream;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+public class GerritClusterAmbassadorTest {
+
+  @ParameterizedTest
+  @MethodSource("provideYamlManifests")
+  public void expectedGerritClusterAmbassadorComponentsCreated(
+      String inputFile, Map<String, String> expectedOutputFileNames)
+      throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
+          IllegalAccessException, InvocationTargetException {
+    GerritNetwork gerritNetwork =
+        ReconcilerUtils.loadYaml(GerritNetwork.class, this.getClass(), inputFile);
+
+    for (Map.Entry<String, String> entry : expectedOutputFileNames.entrySet()) {
+      String className = entry.getKey();
+      String expectedOutputFile = entry.getValue();
+
+      Class<?> clazz = Class.forName(className);
+      Object dependentObject = clazz.getDeclaredConstructor(new Class[] {}).newInstance();
+
+      if (dependentObject instanceof MappingDependentResourceInterface) {
+        MappingDependentResourceInterface dependent =
+            (MappingDependentResourceInterface) dependentObject;
+        Mapping result = dependent.desired(gerritNetwork, null);
+        Mapping expected =
+            ReconcilerUtils.loadYaml(Mapping.class, this.getClass(), expectedOutputFile);
+        assertThat(result.getSpec()).isEqualTo(expected.getSpec());
+      } else if (dependentObject instanceof GerritClusterTLSContext) {
+        GerritClusterTLSContext dependent = (GerritClusterTLSContext) dependentObject;
+        TLSContext result = dependent.desired(gerritNetwork, null);
+        TLSContext expected =
+            ReconcilerUtils.loadYaml(TLSContext.class, this.getClass(), expectedOutputFile);
+        assertThat(result.getSpec()).isEqualTo(expected.getSpec());
+      } else if (dependentObject instanceof GerritClusterHost) {
+        GerritClusterHost dependent = (GerritClusterHost) dependentObject;
+        Host result = dependent.desired(gerritNetwork, null);
+        Host expected = ReconcilerUtils.loadYaml(Host.class, this.getClass(), expectedOutputFile);
+        assertThat(result.getSpec()).isEqualTo(expected.getSpec());
+      }
+    }
+  }
+
+  private static Stream<Arguments> provideYamlManifests() {
+    return Stream.of(
+        Arguments.of(
+            "../../gerritnetwork_primary_replica_tls.yaml",
+            Map.of(
+                GerritClusterMappingGETReplica.class.getName(),
+                    "mappingGETReplica_primary_replica.yaml",
+                GerritClusterMappingPOSTReplica.class.getName(),
+                    "mappingPOSTReplica_primary_replica.yaml",
+                GerritClusterMappingPrimary.class.getName(), "mappingPrimary_primary_replica.yaml",
+                GerritClusterTLSContext.class.getName(), "tlscontext.yaml")),
+        Arguments.of(
+            "../../gerritnetwork_primary_replica_tls_create_host.yaml",
+            Map.of(
+                GerritClusterMappingGETReplica.class.getName(),
+                    "mappingGETReplica_primary_replica.yaml",
+                GerritClusterMappingPOSTReplica.class.getName(),
+                    "mappingPOSTReplica_primary_replica.yaml",
+                GerritClusterMappingPrimary.class.getName(), "mappingPrimary_primary_replica.yaml",
+                GerritClusterTLSContext.class.getName(), "tlscontext.yaml",
+                GerritClusterHost.class.getName(), "host_with_tls.yaml")),
+        Arguments.of(
+            "../../gerritnetwork_primary_replica.yaml",
+            Map.of(
+                GerritClusterMappingGETReplica.class.getName(),
+                    "mappingGETReplica_primary_replica.yaml",
+                GerritClusterMappingPOSTReplica.class.getName(),
+                    "mappingPOSTReplica_primary_replica.yaml",
+                GerritClusterMappingPrimary.class.getName(),
+                    "mappingPrimary_primary_replica.yaml")),
+        Arguments.of(
+            "../../gerritnetwork_primary_replica_create_host.yaml",
+            Map.of(
+                GerritClusterMappingGETReplica.class.getName(),
+                    "mappingGETReplica_primary_replica.yaml",
+                GerritClusterMappingPOSTReplica.class.getName(),
+                    "mappingPOSTReplica_primary_replica.yaml",
+                GerritClusterMappingPrimary.class.getName(), "mappingPrimary_primary_replica.yaml",
+                GerritClusterHost.class.getName(), "host.yaml")),
+        Arguments.of(
+            "../../gerritnetwork_primary.yaml",
+            Map.of(GerritClusterMapping.class.getName(), "mapping_primary.yaml")),
+        Arguments.of(
+            "../../gerritnetwork_replica.yaml",
+            Map.of(GerritClusterMapping.class.getName(), "mapping_replica.yaml")),
+        Arguments.of(
+            "../../gerritnetwork_receiver_replica.yaml",
+            Map.of(
+                GerritClusterMapping.class.getName(), "mapping_replica.yaml",
+                GerritClusterMappingReceiver.class.getName(), "mapping_receiver.yaml",
+                GerritClusterMappingReceiverGET.class.getName(), "mappingGET_receiver.yaml")),
+        Arguments.of(
+            "../../gerritnetwork_receiver_replica_tls.yaml",
+            Map.of(
+                GerritClusterMapping.class.getName(), "mapping_replica.yaml",
+                GerritClusterMappingReceiver.class.getName(), "mapping_receiver.yaml",
+                GerritClusterMappingReceiverGET.class.getName(), "mappingGET_receiver.yaml",
+                GerritClusterTLSContext.class.getName(), "tlscontext.yaml")));
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/network/ingress/dependent/GerritClusterIngressTest.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/network/ingress/dependent/GerritClusterIngressTest.java
new file mode 100644
index 0000000..a2e6a24
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/network/ingress/dependent/GerritClusterIngressTest.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ingress.dependent;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.fabric8.kubernetes.api.model.networking.v1.Ingress;
+import io.javaoperatorsdk.operator.ReconcilerUtils;
+import java.util.stream.Stream;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+public class GerritClusterIngressTest {
+  @ParameterizedTest
+  @MethodSource("provideYamlManifests")
+  public void expectedGerritClusterIngressCreated(String inputFile, String expectedOutputFile) {
+    GerritClusterIngress dependent = new GerritClusterIngress();
+    Ingress result =
+        dependent.desired(
+            ReconcilerUtils.loadYaml(GerritNetwork.class, this.getClass(), inputFile), null);
+    Ingress expected = ReconcilerUtils.loadYaml(Ingress.class, this.getClass(), expectedOutputFile);
+    assertThat(result.getSpec()).isEqualTo(expected.getSpec());
+    assertThat(result.getMetadata().getAnnotations())
+        .containsExactlyEntriesIn(expected.getMetadata().getAnnotations());
+  }
+
+  private static Stream<Arguments> provideYamlManifests() {
+    return Stream.of(
+        Arguments.of(
+            "../../gerritnetwork_primary_replica_tls.yaml", "ingress_primary_replica_tls.yaml"),
+        Arguments.of("../../gerritnetwork_primary_replica.yaml", "ingress_primary_replica.yaml"),
+        Arguments.of("../../gerritnetwork_primary.yaml", "ingress_primary.yaml"),
+        Arguments.of("../../gerritnetwork_replica.yaml", "ingress_replica.yaml"),
+        Arguments.of("../../gerritnetwork_receiver_replica.yaml", "ingress_receiver_replica.yaml"),
+        Arguments.of(
+            "../../gerritnetwork_receiver_replica_tls.yaml", "ingress_receiver_replica_tls.yaml"));
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritClusterIstioTest.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritClusterIstioTest.java
new file mode 100644
index 0000000..eaed636
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritClusterIstioTest.java
@@ -0,0 +1,88 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.istio.dependent;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.fabric8.istio.api.networking.v1beta1.Gateway;
+import io.fabric8.istio.api.networking.v1beta1.VirtualService;
+import io.javaoperatorsdk.operator.ReconcilerUtils;
+import java.util.stream.Stream;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+public class GerritClusterIstioTest {
+  @ParameterizedTest
+  @MethodSource("provideYamlManifests")
+  public void expectedGerritClusterIstioComponentsCreated(
+      String inputFile, String expectedGatewayOutputFile, String expectedVirtualServiceOutputFile) {
+    GerritNetwork gerritNetwork =
+        ReconcilerUtils.loadYaml(GerritNetwork.class, this.getClass(), inputFile);
+    GerritClusterIstioGateway gatewayDependent = new GerritClusterIstioGateway();
+    Gateway gatewayResult = gatewayDependent.desired(gerritNetwork, null);
+    Gateway expectedGateway =
+        ReconcilerUtils.loadYaml(Gateway.class, this.getClass(), expectedGatewayOutputFile);
+    assertThat(gatewayResult.getSpec()).isEqualTo(expectedGateway.getSpec());
+
+    GerritIstioVirtualService virtualServiceDependent = new GerritIstioVirtualService();
+    VirtualService virtualServiceResult = virtualServiceDependent.desired(gerritNetwork, null);
+    VirtualService expectedVirtualService =
+        ReconcilerUtils.loadYaml(
+            VirtualService.class, this.getClass(), expectedVirtualServiceOutputFile);
+    assertThat(virtualServiceResult.getSpec()).isEqualTo(expectedVirtualService.getSpec());
+  }
+
+  private static Stream<Arguments> provideYamlManifests() {
+    return Stream.of(
+        Arguments.of(
+            "../../gerritnetwork_primary_replica_tls.yaml",
+            "gateway_tls.yaml",
+            "virtualservice_primary_replica.yaml"),
+        Arguments.of(
+            "../../gerritnetwork_primary_replica.yaml",
+            "gateway.yaml",
+            "virtualservice_primary_replica.yaml"),
+        Arguments.of(
+            "../../gerritnetwork_primary.yaml", "gateway.yaml", "virtualservice_primary.yaml"),
+        Arguments.of(
+            "../../gerritnetwork_replica.yaml", "gateway.yaml", "virtualservice_replica.yaml"),
+        Arguments.of(
+            "../../gerritnetwork_receiver_replica.yaml",
+            "gateway.yaml",
+            "virtualservice_receiver_replica.yaml"),
+        Arguments.of(
+            "../../gerritnetwork_receiver_replica_tls.yaml",
+            "gateway_tls.yaml",
+            "virtualservice_receiver_replica.yaml"),
+        Arguments.of(
+            "../../gerritnetwork_primary_ssh.yaml",
+            "gateway_primary_ssh.yaml",
+            "virtualservice_primary_ssh.yaml"),
+        Arguments.of(
+            "../../gerritnetwork_replica_ssh.yaml",
+            "gateway_replica_ssh.yaml",
+            "virtualservice_replica_ssh.yaml"),
+        Arguments.of(
+            "../../gerritnetwork_primary_replica_ssh.yaml",
+            "gateway_primary_replica_ssh.yaml",
+            "virtualservice_primary_replica_ssh.yaml"),
+        Arguments.of(
+            "../../gerritnetwork_receiver_replica_ssh.yaml",
+            "gateway_receiver_replica_ssh.yaml",
+            "virtualservice_receiver_replica_ssh.yaml"));
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/AbstractClusterManagedReceiverE2E.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/AbstractClusterManagedReceiverE2E.java
new file mode 100644
index 0000000..557e140
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/AbstractClusterManagedReceiverE2E.java
@@ -0,0 +1,110 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.receiver;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.gerrit.k8s.operator.test.AbstractGerritOperatorE2ETest;
+import com.google.gerrit.k8s.operator.test.ReceiverUtil;
+import com.google.gerrit.k8s.operator.test.TestGerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplate;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplateSpec.GerritMode;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.ReceiverTemplate;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.ReceiverTemplateSpec;
+import io.fabric8.kubernetes.api.model.ObjectMeta;
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+import java.io.File;
+import java.net.URL;
+import java.nio.file.Path;
+import org.apache.http.client.utils.URIBuilder;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.RefSpec;
+import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+public abstract class AbstractClusterManagedReceiverE2E extends AbstractGerritOperatorE2ETest {
+  private static final String GERRIT_NAME = "gerrit";
+  private ReceiverTemplate receiver;
+  private GerritTemplate gerrit;
+
+  @BeforeEach
+  public void setupComponents() throws Exception {
+    gerrit = TestGerrit.createGerritTemplate(GERRIT_NAME, GerritMode.REPLICA);
+    gerritCluster.addGerrit(gerrit);
+
+    receiver = new ReceiverTemplate();
+    ObjectMeta receiverMeta = new ObjectMetaBuilder().withName("receiver").build();
+    receiver.setMetadata(receiverMeta);
+    ReceiverTemplateSpec receiverTemplateSpec = new ReceiverTemplateSpec();
+    receiverTemplateSpec.setReplicas(2);
+    receiverTemplateSpec.setCredentialSecretRef(ReceiverUtil.CREDENTIALS_SECRET_NAME);
+    receiver.setSpec(receiverTemplateSpec);
+    gerritCluster.setReceiver(receiver);
+    gerritCluster.deploy();
+  }
+
+  @Test
+  public void testProjectLifecycle(@TempDir Path tempDir) throws Exception {
+    GerritCluster cluster = gerritCluster.getGerritCluster();
+    assertProjectLifecycle(cluster, tempDir);
+  }
+
+  private void assertProjectLifecycle(GerritCluster cluster, Path tempDir) throws Exception {
+    assertThat(
+        ReceiverUtil.sendReceiverApiRequest(cluster, "PUT", "/a/projects/test.git"),
+        is(equalTo(201)));
+    CredentialsProvider gerritCredentials =
+        new UsernamePasswordCredentialsProvider(
+            testProps.getGerritUser(), testProps.getGerritPwd());
+    Git git =
+        Git.cloneRepository()
+            .setURI(getGerritUrl("/test.git").toString())
+            .setCredentialsProvider(gerritCredentials)
+            .setDirectory(tempDir.toFile())
+            .call();
+    new File("test.txt").createNewFile();
+    git.add().addFilepattern(".").call();
+    RevCommit commit = git.commit().setMessage("test commit").call();
+    git.push()
+        .setCredentialsProvider(
+            new UsernamePasswordCredentialsProvider(
+                ReceiverUtil.RECEIVER_TEST_USER, ReceiverUtil.RECEIVER_TEST_PASSWORD))
+        .setRefSpecs(new RefSpec("refs/heads/master"))
+        .call();
+    assertTrue(
+        git.lsRemote().setCredentialsProvider(gerritCredentials).setRemote("origin").call().stream()
+            .anyMatch(ref -> ref.getObjectId().equals(commit.getId())));
+    assertThat(
+        ReceiverUtil.sendReceiverApiRequest(cluster, "DELETE", "/a/projects/test.git"),
+        is(equalTo(204)));
+  }
+
+  private URL getGerritUrl(String path) throws Exception {
+    return new URIBuilder()
+        .setScheme("https")
+        .setHost(gerritCluster.getHostname())
+        .setPath(path)
+        .build()
+        .toURL();
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/ClusterManagedReceiverWithIngressE2E.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/ClusterManagedReceiverWithIngressE2E.java
new file mode 100644
index 0000000..4dd8bd3
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/ClusterManagedReceiverWithIngressE2E.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.receiver;
+
+import com.google.gerrit.k8s.operator.network.IngressType;
+
+public class ClusterManagedReceiverWithIngressE2E extends AbstractClusterManagedReceiverE2E {
+
+  @Override
+  protected IngressType getIngressType() {
+    return IngressType.INGRESS;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/ClusterManagedReceiverWithIstioE2E.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/ClusterManagedReceiverWithIstioE2E.java
new file mode 100644
index 0000000..e76303c
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/ClusterManagedReceiverWithIstioE2E.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.receiver;
+
+import com.google.gerrit.k8s.operator.network.IngressType;
+
+public class ClusterManagedReceiverWithIstioE2E extends AbstractClusterManagedReceiverE2E {
+  @Override
+  protected IngressType getIngressType() {
+    return IngressType.ISTIO;
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/dependent/ReceiverTest.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/dependent/ReceiverTest.java
new file mode 100644
index 0000000..8698b1a
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/dependent/ReceiverTest.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.receiver.dependent;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.Receiver;
+import io.fabric8.kubernetes.api.model.Service;
+import io.fabric8.kubernetes.api.model.apps.Deployment;
+import io.javaoperatorsdk.operator.ReconcilerUtils;
+import java.util.stream.Stream;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+public class ReceiverTest {
+  @ParameterizedTest
+  @MethodSource("provideYamlManifests")
+  public void expectedReceiverComponentsCreated(
+      String inputFile, String expectedDeployment, String expectedService) {
+    Receiver input = ReconcilerUtils.loadYaml(Receiver.class, this.getClass(), inputFile);
+    ReceiverDeployment dependentDeployment = new ReceiverDeployment();
+    assertThat(dependentDeployment.desired(input, null))
+        .isEqualTo(ReconcilerUtils.loadYaml(Deployment.class, this.getClass(), expectedDeployment));
+
+    ReceiverService dependentService = new ReceiverService();
+    assertThat(dependentService.desired(input, null))
+        .isEqualTo(ReconcilerUtils.loadYaml(Service.class, this.getClass(), expectedService));
+  }
+
+  private static Stream<Arguments> provideYamlManifests() {
+    return Stream.of(
+        Arguments.of("../receiver.yaml", "deployment.yaml", "service.yaml"),
+        Arguments.of("../receiver_minimal.yaml", "deployment_minimal.yaml", "service.yaml"));
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/server/GerritAdmissionWebhookTest.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/server/GerritAdmissionWebhookTest.java
new file mode 100644
index 0000000..32b342d
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/server/GerritAdmissionWebhookTest.java
@@ -0,0 +1,182 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.server;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.gerrit.k8s.operator.test.TestAdmissionWebhookServer;
+import com.google.gerrit.k8s.operator.v1alpha.admission.servlet.GerritAdmissionWebhook;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritClusterSpec;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritSpec;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplateSpec.GerritMode;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.Receiver;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GerritClusterIngressConfig;
+import io.fabric8.kubernetes.api.model.DefaultKubernetesResourceList;
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.fabric8.kubernetes.api.model.ObjectMeta;
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+import io.fabric8.kubernetes.api.model.admission.v1.AdmissionRequest;
+import io.fabric8.kubernetes.api.model.admission.v1.AdmissionReview;
+import io.fabric8.kubernetes.client.server.mock.KubernetesServer;
+import io.fabric8.kubernetes.internal.KubernetesDeserializer;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Map;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Rule;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestInstance.Lifecycle;
+
+@TestInstance(Lifecycle.PER_CLASS)
+public class GerritAdmissionWebhookTest {
+  private static final String NAMESPACE = "test";
+  private static final String LIST_GERRITS_PATH =
+      String.format(
+          "/apis/%s/namespaces/%s/%s",
+          HasMetadata.getApiVersion(Gerrit.class), NAMESPACE, HasMetadata.getPlural(Gerrit.class));
+  private static final String LIST_GERRIT_CLUSTERS_PATH =
+      String.format(
+          "/apis/%s/namespaces/%s/%s",
+          HasMetadata.getApiVersion(GerritCluster.class),
+          NAMESPACE,
+          HasMetadata.getPlural(GerritCluster.class));
+  private TestAdmissionWebhookServer server;
+
+  @Rule public KubernetesServer kubernetesServer = new KubernetesServer();
+
+  @BeforeAll
+  public void setup() throws Exception {
+    KubernetesDeserializer.registerCustomKind(
+        "gerritoperator.google.com/v1alpha2", "Gerrit", Gerrit.class);
+    KubernetesDeserializer.registerCustomKind(
+        "gerritoperator.google.com/v1alpha1", "Receiver", Receiver.class);
+    server = new TestAdmissionWebhookServer();
+
+    kubernetesServer.before();
+
+    GerritAdmissionWebhook webhook = new GerritAdmissionWebhook();
+    server.registerWebhook(webhook);
+    server.start();
+  }
+
+  @Test
+  public void testInvalidGerritConfigRejected() throws Exception {
+    String clusterName = "gerrit";
+    Config gerritConfig = new Config();
+    gerritConfig.setString("container", null, "user", "gerrit");
+    Gerrit gerrit = createGerrit(clusterName, gerritConfig);
+    kubernetesServer
+        .expect()
+        .get()
+        .withPath(LIST_GERRITS_PATH)
+        .andReturn(HttpURLConnection.HTTP_OK, new DefaultKubernetesResourceList<Gerrit>())
+        .times(2);
+
+    mockGerritCluster(clusterName);
+
+    HttpURLConnection http = sendAdmissionRequest(gerrit);
+
+    AdmissionReview response =
+        new ObjectMapper().readValue(http.getInputStream(), AdmissionReview.class);
+
+    assertThat(http.getResponseCode(), is(equalTo(HttpServletResponse.SC_OK)));
+    assertThat(response.getResponse().getAllowed(), is(true));
+
+    gerritConfig.setString("container", null, "user", "invalid");
+    Gerrit gerrit2 = createGerrit(clusterName, gerritConfig);
+    HttpURLConnection http2 = sendAdmissionRequest(gerrit2);
+
+    AdmissionReview response2 =
+        new ObjectMapper().readValue(http2.getInputStream(), AdmissionReview.class);
+
+    assertThat(http2.getResponseCode(), is(equalTo(HttpServletResponse.SC_OK)));
+    assertThat(response2.getResponse().getAllowed(), is(false));
+  }
+
+  private void mockGerritCluster(String name) {
+    GerritCluster cluster = new GerritCluster();
+    cluster.setMetadata(new ObjectMetaBuilder().withName(name).withNamespace(NAMESPACE).build());
+    GerritClusterSpec clusterSpec = new GerritClusterSpec();
+    GerritClusterIngressConfig ingressConfig = new GerritClusterIngressConfig();
+    ingressConfig.setEnabled(false);
+    clusterSpec.setIngress(ingressConfig);
+    clusterSpec.setServerId("test");
+    cluster.setSpec(clusterSpec);
+
+    kubernetesServer
+        .expect()
+        .get()
+        .withPath(LIST_GERRIT_CLUSTERS_PATH + "/" + name)
+        .andReturn(HttpURLConnection.HTTP_OK, cluster)
+        .always();
+  }
+
+  private Gerrit createGerrit(String cluster, Config gerritConfig) {
+    ObjectMeta meta =
+        new ObjectMetaBuilder()
+            .withName(RandomStringUtils.random(10))
+            .withNamespace(NAMESPACE)
+            .build();
+    GerritSpec gerritSpec = new GerritSpec();
+    gerritSpec.setMode(GerritMode.PRIMARY);
+    if (gerritConfig != null) {
+      gerritSpec.setConfigFiles(Map.of("gerrit.config", gerritConfig.toText()));
+    }
+    Gerrit gerrit = new Gerrit();
+    gerrit.setMetadata(meta);
+    gerrit.setSpec(gerritSpec);
+    return gerrit;
+  }
+
+  private HttpURLConnection sendAdmissionRequest(Gerrit gerrit)
+      throws MalformedURLException, IOException {
+    HttpURLConnection http =
+        (HttpURLConnection)
+            new URL("http://localhost:8080/admission/v1alpha/gerrit").openConnection();
+    http.setRequestMethod(HttpMethod.POST.asString());
+    http.setRequestProperty("Content-Type", "application/json");
+    http.setDoOutput(true);
+
+    AdmissionRequest admissionReq = new AdmissionRequest();
+    admissionReq.setObject(gerrit);
+    AdmissionReview admissionReview = new AdmissionReview();
+    admissionReview.setRequest(admissionReq);
+
+    try (OutputStream os = http.getOutputStream()) {
+      byte[] input = new ObjectMapper().writer().writeValueAsBytes(admissionReview);
+      os.write(input, 0, input.length);
+    }
+    return http;
+  }
+
+  @AfterAll
+  public void shutdown() throws Exception {
+    server.stop();
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/server/GerritClusterAdmissionWebhookTest.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/server/GerritClusterAdmissionWebhookTest.java
new file mode 100644
index 0000000..55526c7
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/server/GerritClusterAdmissionWebhookTest.java
@@ -0,0 +1,195 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.server;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.gerrit.k8s.operator.test.ReceiverUtil;
+import com.google.gerrit.k8s.operator.test.TestAdmissionWebhookServer;
+import com.google.gerrit.k8s.operator.test.TestGerrit;
+import com.google.gerrit.k8s.operator.test.TestGerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.admission.servlet.GerritAdmissionWebhook;
+import com.google.gerrit.k8s.operator.v1alpha.admission.servlet.GerritClusterAdmissionWebhook;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplate;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplateSpec.GerritMode;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.Receiver;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.ReceiverTemplate;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.ReceiverTemplateSpec;
+import io.fabric8.kubernetes.api.model.ObjectMeta;
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+import io.fabric8.kubernetes.api.model.admission.v1.AdmissionRequest;
+import io.fabric8.kubernetes.api.model.admission.v1.AdmissionReview;
+import io.fabric8.kubernetes.client.server.mock.KubernetesServer;
+import io.fabric8.kubernetes.internal.KubernetesDeserializer;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Rule;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestInstance.Lifecycle;
+
+@TestInstance(Lifecycle.PER_CLASS)
+public class GerritClusterAdmissionWebhookTest {
+  private static final String NAMESPACE = "test";
+  private TestAdmissionWebhookServer server;
+
+  @Rule public KubernetesServer kubernetesServer = new KubernetesServer();
+
+  @BeforeAll
+  public void setup() throws Exception {
+    KubernetesDeserializer.registerCustomKind(
+        "gerritoperator.google.com/v1alpha2", "Gerrit", Gerrit.class);
+    KubernetesDeserializer.registerCustomKind(
+        "gerritoperator.google.com/v1alpha2", "GerritCluster", GerritCluster.class);
+    KubernetesDeserializer.registerCustomKind(
+        "gerritoperator.google.com/v1alpha2", "Receiver", Receiver.class);
+    server = new TestAdmissionWebhookServer();
+
+    kubernetesServer.before();
+
+    server.registerWebhook(new GerritClusterAdmissionWebhook());
+    server.registerWebhook(new GerritAdmissionWebhook());
+    server.start();
+  }
+
+  @Test
+  public void testOnlySinglePrimaryGerritIsAcceptedPerGerritCluster() throws Exception {
+    Config cfg = new Config();
+    cfg.fromText(TestGerrit.DEFAULT_GERRIT_CONFIG);
+    GerritTemplate gerrit1 = TestGerrit.createGerritTemplate("gerrit1", GerritMode.PRIMARY, cfg);
+    TestGerritCluster gerritCluster =
+        new TestGerritCluster(kubernetesServer.getClient(), NAMESPACE);
+    gerritCluster.addGerrit(gerrit1);
+    GerritCluster cluster = gerritCluster.build();
+
+    HttpURLConnection http = sendAdmissionRequest(cluster);
+
+    AdmissionReview response =
+        new ObjectMapper().readValue(http.getInputStream(), AdmissionReview.class);
+
+    assertThat(http.getResponseCode(), is(equalTo(HttpServletResponse.SC_OK)));
+    assertThat(response.getResponse().getAllowed(), is(true));
+
+    GerritTemplate gerrit2 = TestGerrit.createGerritTemplate("gerrit2", GerritMode.PRIMARY, cfg);
+    gerritCluster.addGerrit(gerrit2);
+    HttpURLConnection http2 = sendAdmissionRequest(gerritCluster.build());
+
+    AdmissionReview response2 =
+        new ObjectMapper().readValue(http2.getInputStream(), AdmissionReview.class);
+
+    assertThat(http2.getResponseCode(), is(equalTo(HttpServletResponse.SC_OK)));
+    assertThat(response2.getResponse().getAllowed(), is(false));
+    assertThat(
+        response2.getResponse().getStatus().getCode(),
+        is(equalTo(HttpServletResponse.SC_CONFLICT)));
+  }
+
+  @Test
+  public void testPrimaryGerritAndReceiverAreNotAcceptedInSameGerritCluster() throws Exception {
+    Config cfg = new Config();
+    cfg.fromText(TestGerrit.DEFAULT_GERRIT_CONFIG);
+    GerritTemplate gerrit = TestGerrit.createGerritTemplate("gerrit1", GerritMode.PRIMARY, cfg);
+    TestGerritCluster gerritCluster =
+        new TestGerritCluster(kubernetesServer.getClient(), NAMESPACE);
+    gerritCluster.addGerrit(gerrit);
+
+    ReceiverTemplate receiver = new ReceiverTemplate();
+    ObjectMeta receiverMeta = new ObjectMetaBuilder().withName("receiver").build();
+    receiver.setMetadata(receiverMeta);
+    ReceiverTemplateSpec receiverTemplateSpec = new ReceiverTemplateSpec();
+    receiverTemplateSpec.setReplicas(2);
+    receiverTemplateSpec.setCredentialSecretRef(ReceiverUtil.CREDENTIALS_SECRET_NAME);
+    receiver.setSpec(receiverTemplateSpec);
+
+    gerritCluster.setReceiver(receiver);
+    HttpURLConnection http2 = sendAdmissionRequest(gerritCluster.build());
+
+    AdmissionReview response2 =
+        new ObjectMapper().readValue(http2.getInputStream(), AdmissionReview.class);
+
+    assertThat(http2.getResponseCode(), is(equalTo(HttpServletResponse.SC_OK)));
+    assertThat(response2.getResponse().getAllowed(), is(false));
+    assertThat(
+        response2.getResponse().getStatus().getCode(),
+        is(equalTo(HttpServletResponse.SC_CONFLICT)));
+  }
+
+  @Test
+  public void testPrimaryAndReplicaAreAcceptedInSameGerritCluster() throws Exception {
+    Config cfg = new Config();
+    cfg.fromText(TestGerrit.DEFAULT_GERRIT_CONFIG);
+    GerritTemplate gerrit1 = TestGerrit.createGerritTemplate("gerrit1", GerritMode.PRIMARY, cfg);
+    TestGerritCluster gerritCluster =
+        new TestGerritCluster(kubernetesServer.getClient(), NAMESPACE);
+    gerritCluster.addGerrit(gerrit1);
+
+    HttpURLConnection http = sendAdmissionRequest(gerritCluster.build());
+
+    AdmissionReview response =
+        new ObjectMapper().readValue(http.getInputStream(), AdmissionReview.class);
+
+    assertThat(http.getResponseCode(), is(equalTo(HttpServletResponse.SC_OK)));
+    assertThat(response.getResponse().getAllowed(), is(true));
+
+    GerritTemplate gerrit2 = TestGerrit.createGerritTemplate("gerrit2", GerritMode.REPLICA, cfg);
+    gerritCluster.addGerrit(gerrit2);
+    HttpURLConnection http2 = sendAdmissionRequest(gerritCluster.build());
+
+    AdmissionReview response2 =
+        new ObjectMapper().readValue(http2.getInputStream(), AdmissionReview.class);
+
+    assertThat(http.getResponseCode(), is(equalTo(HttpServletResponse.SC_OK)));
+    assertThat(response2.getResponse().getAllowed(), is(true));
+  }
+
+  private HttpURLConnection sendAdmissionRequest(GerritCluster gerritCluster)
+      throws MalformedURLException, IOException {
+    HttpURLConnection http =
+        (HttpURLConnection)
+            new URL("http://localhost:8080/admission/v1alpha/gerritcluster").openConnection();
+    http.setRequestMethod(HttpMethod.POST.asString());
+    http.setRequestProperty("Content-Type", "application/json");
+    http.setDoOutput(true);
+
+    AdmissionRequest admissionReq = new AdmissionRequest();
+    admissionReq.setObject(gerritCluster);
+    AdmissionReview admissionReview = new AdmissionReview();
+    admissionReview.setRequest(admissionReq);
+
+    try (OutputStream os = http.getOutputStream()) {
+      byte[] input = new ObjectMapper().writer().writeValueAsBytes(admissionReview);
+      os.write(input, 0, input.length);
+    }
+    return http;
+  }
+
+  @AfterAll
+  public void shutdown() throws Exception {
+    server.stop();
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/server/GitGcAdmissionWebhookTest.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/server/GitGcAdmissionWebhookTest.java
new file mode 100644
index 0000000..5553e95
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/server/GitGcAdmissionWebhookTest.java
@@ -0,0 +1,256 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.server;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.gerrit.k8s.operator.test.TestAdmissionWebhookServer;
+import com.google.gerrit.k8s.operator.v1alpha.admission.servlet.GitGcAdmissionWebhook;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gitgc.GitGarbageCollection;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gitgc.GitGarbageCollectionSpec;
+import io.fabric8.kubernetes.api.model.DefaultKubernetesResourceList;
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+import io.fabric8.kubernetes.api.model.admission.v1.AdmissionRequest;
+import io.fabric8.kubernetes.api.model.admission.v1.AdmissionReview;
+import io.fabric8.kubernetes.client.server.mock.KubernetesServer;
+import io.fabric8.kubernetes.internal.KubernetesDeserializer;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.List;
+import java.util.Set;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.eclipse.jetty.http.HttpMethod;
+import org.junit.Rule;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestInstance.Lifecycle;
+
+@TestInstance(Lifecycle.PER_CLASS)
+public class GitGcAdmissionWebhookTest {
+  private static final String NAMESPACE = "test";
+  private static final String LIST_GITGCS_PATH =
+      String.format(
+          "/apis/%s/namespaces/%s/%s",
+          HasMetadata.getApiVersion(GitGarbageCollection.class),
+          NAMESPACE,
+          HasMetadata.getPlural(GitGarbageCollection.class));
+  private TestAdmissionWebhookServer server;
+
+  @Rule public KubernetesServer kubernetesServer = new KubernetesServer();
+
+  @BeforeAll
+  public void setup() throws Exception {
+    KubernetesDeserializer.registerCustomKind(
+        "gerritoperator.google.com/v1alpha16", "GerritCluster", GerritCluster.class);
+    KubernetesDeserializer.registerCustomKind(
+        "gerritoperator.google.com/v1alpha1", "GitGarbageCollection", GitGarbageCollection.class);
+    server = new TestAdmissionWebhookServer();
+
+    kubernetesServer.before();
+
+    GitGcAdmissionWebhook webhook = new GitGcAdmissionWebhook(kubernetesServer.getClient());
+    server.registerWebhook(webhook);
+    server.start();
+  }
+
+  @Test
+  @DisplayName("Only a single GitGC that works on all projects in site is allowed.")
+  public void testOnlySingleGitGcWorkingOnAllProjectsIsAllowed() throws Exception {
+    GitGarbageCollection gitGc = createCompleteGitGc();
+    kubernetesServer
+        .expect()
+        .get()
+        .withPath(LIST_GITGCS_PATH)
+        .andReturn(
+            HttpURLConnection.HTTP_OK, new DefaultKubernetesResourceList<GitGarbageCollection>())
+        .once();
+
+    HttpURLConnection http = sendAdmissionRequest(gitGc);
+
+    AdmissionReview response =
+        new ObjectMapper().readValue(http.getInputStream(), AdmissionReview.class);
+
+    assertThat(http.getResponseCode(), is(equalTo(HttpServletResponse.SC_OK)));
+    assertThat(response.getResponse().getAllowed(), is(true));
+
+    DefaultKubernetesResourceList<GitGarbageCollection> existingGitGcs =
+        new DefaultKubernetesResourceList<GitGarbageCollection>();
+    existingGitGcs.setItems(List.of(createCompleteGitGc()));
+    kubernetesServer
+        .expect()
+        .get()
+        .withPath(LIST_GITGCS_PATH)
+        .andReturn(HttpURLConnection.HTTP_OK, existingGitGcs)
+        .once();
+
+    HttpURLConnection http2 = sendAdmissionRequest(gitGc);
+
+    AdmissionReview response2 =
+        new ObjectMapper().readValue(http2.getInputStream(), AdmissionReview.class);
+
+    assertThat(http2.getResponseCode(), is(equalTo(HttpServletResponse.SC_OK)));
+    assertThat(response2.getResponse().getAllowed(), is(false));
+    assertThat(
+        response2.getResponse().getStatus().getCode(),
+        is(equalTo(HttpServletResponse.SC_CONFLICT)));
+  }
+
+  @Test
+  @DisplayName(
+      "A GitGc configured to work on all projects and selective GitGcs are allowed to exist at the same time.")
+  public void testSelectiveAndCompleteGitGcAreAllowedTogether() throws Exception {
+    DefaultKubernetesResourceList<GitGarbageCollection> existingGitGcs =
+        new DefaultKubernetesResourceList<GitGarbageCollection>();
+    existingGitGcs.setItems(List.of(createCompleteGitGc()));
+    kubernetesServer
+        .expect()
+        .get()
+        .withPath(LIST_GITGCS_PATH)
+        .andReturn(HttpURLConnection.HTTP_OK, existingGitGcs)
+        .once();
+
+    GitGarbageCollection gitGc2 = createGitGcForProjects(Set.of("project3"));
+    HttpURLConnection http2 = sendAdmissionRequest(gitGc2);
+
+    AdmissionReview response2 =
+        new ObjectMapper().readValue(http2.getInputStream(), AdmissionReview.class);
+
+    assertThat(http2.getResponseCode(), is(equalTo(HttpServletResponse.SC_OK)));
+    assertThat(response2.getResponse().getAllowed(), is(true));
+  }
+
+  @Test
+  @DisplayName("Multiple selectve GitGcs working on a different set of projects are allowed.")
+  public void testNonConflictingSelectiveGcsAreAllowed() throws Exception {
+    GitGarbageCollection gitGc = createGitGcForProjects(Set.of("project1", "project2"));
+    DefaultKubernetesResourceList<GitGarbageCollection> existingGitGcs =
+        new DefaultKubernetesResourceList<GitGarbageCollection>();
+    existingGitGcs.setItems(List.of(gitGc));
+    kubernetesServer
+        .expect()
+        .get()
+        .withPath(LIST_GITGCS_PATH)
+        .andReturn(HttpURLConnection.HTTP_OK, existingGitGcs)
+        .once();
+
+    GitGarbageCollection gitGc2 = createGitGcForProjects(Set.of("project3"));
+    HttpURLConnection http2 = sendAdmissionRequest(gitGc2);
+
+    AdmissionReview response2 =
+        new ObjectMapper().readValue(http2.getInputStream(), AdmissionReview.class);
+
+    assertThat(http2.getResponseCode(), is(equalTo(HttpServletResponse.SC_OK)));
+    assertThat(response2.getResponse().getAllowed(), is(true));
+  }
+
+  @Test
+  @DisplayName("Multiple selectve GitGcs working on the same project(s) are not allowed.")
+  public void testConflictingSelectiveGcsNotAllowed() throws Exception {
+    GitGarbageCollection gitGc = createGitGcForProjects(Set.of("project1", "project2"));
+    kubernetesServer
+        .expect()
+        .get()
+        .withPath(LIST_GITGCS_PATH)
+        .andReturn(
+            HttpURLConnection.HTTP_OK, new DefaultKubernetesResourceList<GitGarbageCollection>())
+        .once();
+
+    HttpURLConnection http = sendAdmissionRequest(gitGc);
+
+    AdmissionReview response =
+        new ObjectMapper().readValue(http.getInputStream(), AdmissionReview.class);
+
+    assertThat(http.getResponseCode(), is(equalTo(HttpServletResponse.SC_OK)));
+    assertThat(response.getResponse().getAllowed(), is(true));
+
+    DefaultKubernetesResourceList<GitGarbageCollection> existingGitGcs =
+        new DefaultKubernetesResourceList<GitGarbageCollection>();
+    existingGitGcs.setItems(List.of(gitGc));
+    kubernetesServer
+        .expect()
+        .get()
+        .withPath(LIST_GITGCS_PATH)
+        .andReturn(HttpURLConnection.HTTP_OK, existingGitGcs)
+        .once();
+
+    GitGarbageCollection gitGc2 = createGitGcForProjects(Set.of("project1"));
+    HttpURLConnection http2 = sendAdmissionRequest(gitGc2);
+
+    AdmissionReview response2 =
+        new ObjectMapper().readValue(http2.getInputStream(), AdmissionReview.class);
+
+    assertThat(http2.getResponseCode(), is(equalTo(HttpServletResponse.SC_OK)));
+    assertThat(response2.getResponse().getAllowed(), is(false));
+    assertThat(
+        response2.getResponse().getStatus().getCode(),
+        is(equalTo(HttpServletResponse.SC_CONFLICT)));
+  }
+
+  private GitGarbageCollection createCompleteGitGc() {
+    return createGitGcForProjects(Set.of());
+  }
+
+  private GitGarbageCollection createGitGcForProjects(Set<String> projects) {
+    GitGarbageCollectionSpec spec = new GitGarbageCollectionSpec();
+    spec.setProjects(projects);
+    GitGarbageCollection gitGc = new GitGarbageCollection();
+    gitGc.setMetadata(
+        new ObjectMetaBuilder()
+            .withName(RandomStringUtils.randomAlphabetic(10))
+            .withUid(RandomStringUtils.randomAlphabetic(10))
+            .withNamespace(NAMESPACE)
+            .build());
+    gitGc.setSpec(spec);
+    return gitGc;
+  }
+
+  private HttpURLConnection sendAdmissionRequest(GitGarbageCollection gitGc)
+      throws MalformedURLException, IOException {
+    HttpURLConnection http =
+        (HttpURLConnection)
+            new URL("http://localhost:8080/admission/v1alpha/gitgc").openConnection();
+    http.setRequestMethod(HttpMethod.POST.asString());
+    http.setRequestProperty("Content-Type", "application/json");
+    http.setDoOutput(true);
+
+    AdmissionRequest admissionReq = new AdmissionRequest();
+    admissionReq.setObject(gitGc);
+    AdmissionReview admissionReview = new AdmissionReview();
+    admissionReview.setRequest(admissionReq);
+
+    try (OutputStream os = http.getOutputStream()) {
+      byte[] input = new ObjectMapper().writer().writeValueAsBytes(admissionReview);
+      os.write(input, 0, input.length);
+    }
+    return http;
+  }
+
+  @AfterAll
+  public void shutdown() throws Exception {
+    server.stop();
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/AbstractGerritOperatorE2ETest.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/AbstractGerritOperatorE2ETest.java
new file mode 100644
index 0000000..ee1aa01
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/AbstractGerritOperatorE2ETest.java
@@ -0,0 +1,140 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.test;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.k8s.operator.cluster.GerritClusterReconciler;
+import com.google.gerrit.k8s.operator.gerrit.GerritReconciler;
+import com.google.gerrit.k8s.operator.gitgc.GitGarbageCollectionReconciler;
+import com.google.gerrit.k8s.operator.network.GerritNetworkReconcilerProvider;
+import com.google.gerrit.k8s.operator.network.IngressType;
+import com.google.gerrit.k8s.operator.receiver.ReceiverReconciler;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gitgc.GitGarbageCollection;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.Receiver;
+import io.fabric8.kubernetes.api.model.Secret;
+import io.fabric8.kubernetes.api.model.SecretBuilder;
+import io.fabric8.kubernetes.client.Config;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import io.fabric8.kubernetes.client.KubernetesClientBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
+import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Base64;
+import java.util.Map;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mockito.Mockito;
+
+public abstract class AbstractGerritOperatorE2ETest {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  protected static final KubernetesClient client = getKubernetesClient();
+  public static final String IMAGE_PULL_SECRET_NAME = "image-pull-secret";
+  public static final TestProperties testProps = new TestProperties();
+
+  protected GerritReconciler gerritReconciler = Mockito.spy(new GerritReconciler(client));
+  protected TestGerritCluster gerritCluster;
+  protected TestSecureConfig secureConfig;
+  protected Secret receiverCredentials;
+
+  @RegisterExtension
+  protected LocallyRunOperatorExtension operator =
+      LocallyRunOperatorExtension.builder()
+          .withNamespaceDeleteTimeout(120)
+          .waitForNamespaceDeletion(true)
+          .withReconciler(new GerritClusterReconciler())
+          .withReconciler(gerritReconciler)
+          .withReconciler(new GitGarbageCollectionReconciler(client))
+          .withReconciler(new ReceiverReconciler(client))
+          .withReconciler(getGerritNetworkReconciler())
+          .build();
+
+  @BeforeEach
+  void setup() {
+    Mockito.reset(gerritReconciler);
+    createImagePullSecret(client, operator.getNamespace());
+
+    secureConfig = new TestSecureConfig(client, testProps, operator.getNamespace());
+    secureConfig.createOrReplace();
+
+    receiverCredentials = ReceiverUtil.createCredentialsSecret(operator.getNamespace());
+
+    client.resource(receiverCredentials).inNamespace(operator.getNamespace()).createOrReplace();
+
+    gerritCluster = new TestGerritCluster(client, operator.getNamespace());
+    gerritCluster.setIngressType(getIngressType());
+    gerritCluster.deploy();
+  }
+
+  @AfterEach
+  void cleanup() {
+    client.resources(Gerrit.class).inNamespace(operator.getNamespace()).delete();
+    client.resources(Receiver.class).inNamespace(operator.getNamespace()).delete();
+    client.resources(GitGarbageCollection.class).inNamespace(operator.getNamespace()).delete();
+    client.resources(GerritCluster.class).inNamespace(operator.getNamespace()).delete();
+    client.resource(receiverCredentials).inNamespace(operator.getNamespace()).delete();
+  }
+
+  private static KubernetesClient getKubernetesClient() {
+    Config config;
+    try {
+      String kubeconfig = System.getenv("KUBECONFIG");
+      if (kubeconfig != null) {
+        config = Config.fromKubeconfig(Files.readString(Path.of(kubeconfig)));
+        return new KubernetesClientBuilder().withConfig(config).build();
+      }
+      logger.atWarning().log("KUBECONFIG variable not set. Using default config.");
+    } catch (IOException e) {
+      logger.atSevere().log("Failed to load kubeconfig. Trying default", e);
+    }
+    return new KubernetesClientBuilder().build();
+  }
+
+  private static void createImagePullSecret(KubernetesClient client, String namespace) {
+    StringBuilder secretBuilder = new StringBuilder();
+    secretBuilder.append("{\"auths\": {\"");
+    secretBuilder.append(testProps.getRegistry());
+    secretBuilder.append("\": {\"auth\": \"");
+    secretBuilder.append(
+        Base64.getEncoder()
+            .encodeToString(
+                String.format("%s:%s", testProps.getRegistryUser(), testProps.getRegistryPwd())
+                    .getBytes()));
+    secretBuilder.append("\"}}}");
+    String data = Base64.getEncoder().encodeToString(secretBuilder.toString().getBytes());
+
+    Secret imagePullSecret =
+        new SecretBuilder()
+            .withType("kubernetes.io/dockerconfigjson")
+            .withNewMetadata()
+            .withName(IMAGE_PULL_SECRET_NAME)
+            .withNamespace(namespace)
+            .endMetadata()
+            .withData(Map.of(".dockerconfigjson", data))
+            .build();
+    client.resource(imagePullSecret).createOrReplace();
+  }
+
+  public Reconciler<GerritNetwork> getGerritNetworkReconciler() {
+    return new GerritNetworkReconcilerProvider(getIngressType()).get();
+  }
+
+  protected abstract IngressType getIngressType();
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/ReceiverUtil.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/ReceiverUtil.java
new file mode 100644
index 0000000..b6cdc5a
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/ReceiverUtil.java
@@ -0,0 +1,75 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.test;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import io.fabric8.kubernetes.api.model.Secret;
+import io.fabric8.kubernetes.api.model.SecretBuilder;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Map;
+import org.apache.commons.codec.digest.Md5Crypt;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.apache.http.client.utils.URIBuilder;
+
+public class ReceiverUtil {
+  public static final String RECEIVER_TEST_USER = "git";
+  public static final String RECEIVER_TEST_PASSWORD = RandomStringUtils.randomAlphanumeric(32);
+  public static final String CREDENTIALS_SECRET_NAME = "receiver-secret";
+  public static final TestProperties testProps = new TestProperties();
+
+  public static int sendReceiverApiRequest(GerritCluster gerritCluster, String method, String path)
+      throws Exception {
+    URL url = getReceiverUrl(gerritCluster, path);
+
+    HttpURLConnection con = (HttpURLConnection) url.openConnection();
+    try {
+      con.setRequestMethod(method);
+      String encodedAuth =
+          Base64.getEncoder()
+              .encodeToString(
+                  String.format("%s:%s", RECEIVER_TEST_USER, RECEIVER_TEST_PASSWORD)
+                      .getBytes(StandardCharsets.UTF_8));
+      con.setRequestProperty("Authorization", "Basic " + encodedAuth);
+      return con.getResponseCode();
+    } finally {
+      con.disconnect();
+    }
+  }
+
+  public static URL getReceiverUrl(GerritCluster gerritCluster, String path) throws Exception {
+    return new URIBuilder()
+        .setScheme("https")
+        .setHost(gerritCluster.getSpec().getIngress().getHost())
+        .setPath(path)
+        .build()
+        .toURL();
+  }
+
+  public static Secret createCredentialsSecret(String namespace) {
+    String enPasswd = Md5Crypt.md5Crypt(RECEIVER_TEST_PASSWORD.getBytes());
+    String htpasswdContent = RECEIVER_TEST_USER + ":" + enPasswd;
+    return new SecretBuilder()
+        .withNewMetadata()
+        .withNamespace(namespace)
+        .withName(CREDENTIALS_SECRET_NAME)
+        .endMetadata()
+        .withData(
+            Map.of(".htpasswd", Base64.getEncoder().encodeToString(htpasswdContent.getBytes())))
+        .build();
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestAdmissionWebhookServer.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestAdmissionWebhookServer.java
new file mode 100644
index 0000000..90c55be
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestAdmissionWebhookServer.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.test;
+
+import com.google.gerrit.k8s.operator.server.AdmissionWebhookServlet;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.servlet.ServletHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+
+public class TestAdmissionWebhookServer {
+  public static final String KEYSTORE_PATH = "/operator/keystore.jks";
+  public static final String KEYSTORE_PWD_FILE = "/operator/keystore.password";
+  public static final int PORT = 8080;
+
+  private final Server server = new Server();
+  private final ServletHandler servletHandler = new ServletHandler();
+
+  public void start() throws Exception {
+    HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory();
+
+    ServerConnector connector = new ServerConnector(server, httpConnectionFactory);
+    connector.setPort(PORT);
+    server.setConnectors(new Connector[] {connector});
+    server.setHandler(servletHandler);
+
+    server.start();
+  }
+
+  public void registerWebhook(AdmissionWebhookServlet webhook) {
+    servletHandler.addServletWithMapping(
+        new ServletHolder(webhook), "/admission/" + webhook.getVersion() + "/" + webhook.getName());
+  }
+
+  public void stop() throws Exception {
+    server.stop();
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestGerrit.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestGerrit.java
new file mode 100644
index 0000000..5762ea5
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestGerrit.java
@@ -0,0 +1,284 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.test;
+
+import static com.google.gerrit.k8s.operator.test.TestSecureConfig.SECURE_CONFIG_SECRET_NAME;
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.k8s.operator.gerrit.dependent.GerritConfigMap;
+import com.google.gerrit.k8s.operator.gerrit.dependent.GerritInitConfigMap;
+import com.google.gerrit.k8s.operator.gerrit.dependent.GerritService;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritSite;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritSpec;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplate;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplateSpec;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplateSpec.GerritMode;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.ContainerImageConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GerritRepositoryConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GerritStorageConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.IngressConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.SharedStorage;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.StorageClassConfig;
+import io.fabric8.kubernetes.api.model.LocalObjectReference;
+import io.fabric8.kubernetes.api.model.ObjectMeta;
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+import io.fabric8.kubernetes.api.model.Quantity;
+import io.fabric8.kubernetes.api.model.ResourceRequirementsBuilder;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+
+public class TestGerrit {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  public static final TestProperties testProps = new TestProperties();
+  public static final String DEFAULT_GERRIT_CONFIG =
+      "[index]\n"
+          + "  type = LUCENE\n"
+          + "[auth]\n"
+          + "  type = LDAP\n"
+          + "[ldap]\n"
+          + "  server = ldap://openldap.openldap.svc.cluster.local:1389\n"
+          + "  accountBase = dc=example,dc=org\n"
+          + "  username = cn=admin,dc=example,dc=org\n"
+          + "[httpd]\n"
+          + "  requestLog = true\n"
+          + "  gracefulStopTimeout = 1m\n"
+          + "[transfer]\n"
+          + "  timeout = 120 s\n"
+          + "[user]\n"
+          + "  name = Gerrit Code Review\n"
+          + "  email = gerrit@example.com\n"
+          + "  anonymousCoward = Unnamed User\n"
+          + "[container]\n"
+          + "  javaOptions = -Xmx4g";
+
+  private final KubernetesClient client;
+  private final String name;
+  private final String namespace;
+  private final GerritMode mode;
+
+  private Gerrit gerrit = new Gerrit();
+  private Config config = defaultConfig();
+
+  public TestGerrit(
+      KubernetesClient client,
+      TestProperties testProps,
+      GerritMode mode,
+      String name,
+      String namespace) {
+    this.client = client;
+    this.mode = mode;
+    this.name = name;
+    this.namespace = namespace;
+  }
+
+  public TestGerrit(
+      KubernetesClient client, TestProperties testProps, String name, String namespace) {
+    this(client, testProps, GerritMode.PRIMARY, name, namespace);
+  }
+
+  public void build() {
+    createGerritCR();
+  }
+
+  public void deploy() {
+    build();
+    client.resource(gerrit).inNamespace(namespace).createOrReplace();
+    waitForGerritReadiness();
+  }
+
+  public void modifyGerritConfig(String section, String key, String value) {
+    config.setString(section, null, key, value);
+  }
+
+  public GerritSpec getSpec() {
+    return gerrit.getSpec();
+  }
+
+  public void setSpec(GerritSpec spec) {
+    gerrit.setSpec(spec);
+    deploy();
+  }
+
+  private static Config defaultConfig() {
+    Config cfg = new Config();
+    try {
+      cfg.fromText(DEFAULT_GERRIT_CONFIG);
+    } catch (ConfigInvalidException e) {
+      throw new IllegalStateException("Illegal default test configuration.");
+    }
+    return cfg;
+  }
+
+  public GerritTemplate createGerritTemplate() throws ConfigInvalidException {
+    return createGerritTemplate(name, mode, config);
+  }
+
+  public static GerritTemplate createGerritTemplate(String name, GerritMode mode)
+      throws ConfigInvalidException {
+    Config cfg = new Config();
+    cfg.fromText(DEFAULT_GERRIT_CONFIG);
+    return createGerritTemplate(name, mode, cfg);
+  }
+
+  public static GerritTemplate createGerritTemplate(String name, GerritMode mode, Config config) {
+    GerritTemplate template = new GerritTemplate();
+    ObjectMeta gerritMeta = new ObjectMetaBuilder().withName(name).build();
+    template.setMetadata(gerritMeta);
+    GerritTemplateSpec gerritSpec = template.getSpec();
+    if (gerritSpec == null) {
+      gerritSpec = new GerritTemplateSpec();
+      GerritSite site = new GerritSite();
+      site.setSize(new Quantity("1Gi"));
+      gerritSpec.setSite(site);
+      gerritSpec.setResources(
+          new ResourceRequirementsBuilder()
+              .withRequests(Map.of("cpu", new Quantity("1"), "memory", new Quantity("5Gi")))
+              .build());
+    }
+    gerritSpec.setMode(mode);
+    gerritSpec.setConfigFiles(Map.of("gerrit.config", config.toText()));
+    gerritSpec.setSecretRef(SECURE_CONFIG_SECRET_NAME);
+    template.setSpec(gerritSpec);
+    return template;
+  }
+
+  private void createGerritCR() {
+    ObjectMeta gerritMeta = new ObjectMetaBuilder().withName(name).withNamespace(namespace).build();
+    gerrit.setMetadata(gerritMeta);
+    GerritSpec gerritSpec = gerrit.getSpec();
+    if (gerritSpec == null) {
+      gerritSpec = new GerritSpec();
+      GerritSite site = new GerritSite();
+      site.setSize(new Quantity("1Gi"));
+      gerritSpec.setSite(site);
+      gerritSpec.setServerId("gerrit-1234");
+      gerritSpec.setResources(
+          new ResourceRequirementsBuilder()
+              .withRequests(Map.of("cpu", new Quantity("1"), "memory", new Quantity("5Gi")))
+              .build());
+    }
+    gerritSpec.setMode(mode);
+    gerritSpec.setConfigFiles(Map.of("gerrit.config", config.toText()));
+    gerritSpec.setSecretRef(SECURE_CONFIG_SECRET_NAME);
+
+    SharedStorage sharedStorage = new SharedStorage();
+    sharedStorage.setSize(Quantity.parse("1Gi"));
+
+    StorageClassConfig storageClassConfig = new StorageClassConfig();
+    storageClassConfig.setReadWriteMany(testProps.getRWMStorageClass());
+
+    GerritStorageConfig gerritStorageConfig = new GerritStorageConfig();
+    gerritStorageConfig.setSharedStorage(sharedStorage);
+    gerritStorageConfig.setStorageClasses(storageClassConfig);
+    gerritSpec.setStorage(gerritStorageConfig);
+
+    GerritRepositoryConfig repoConfig = new GerritRepositoryConfig();
+    repoConfig.setOrg(testProps.getRegistryOrg());
+    repoConfig.setRegistry(testProps.getRegistry());
+    repoConfig.setTag(testProps.getTag());
+
+    ContainerImageConfig containerImageConfig = new ContainerImageConfig();
+    containerImageConfig.setGerritImages(repoConfig);
+    Set<LocalObjectReference> imagePullSecrets = new HashSet<>();
+    imagePullSecrets.add(
+        new LocalObjectReference(AbstractGerritOperatorE2ETest.IMAGE_PULL_SECRET_NAME));
+    containerImageConfig.setImagePullSecrets(imagePullSecrets);
+    gerritSpec.setContainerImages(containerImageConfig);
+
+    IngressConfig ingressConfig = new IngressConfig();
+    ingressConfig.setHost(testProps.getIngressDomain());
+    ingressConfig.setTlsEnabled(false);
+    gerritSpec.setIngress(ingressConfig);
+
+    gerrit.setSpec(gerritSpec);
+  }
+
+  private void waitForGerritReadiness() {
+    logger.atInfo().log("Waiting max 1 minutes for the configmaps to be created.");
+    await()
+        .atMost(1, MINUTES)
+        .untilAsserted(
+            () -> {
+              assertThat(
+                  client
+                      .configMaps()
+                      .inNamespace(namespace)
+                      .withName(GerritConfigMap.getName(gerrit))
+                      .get(),
+                  is(notNullValue()));
+              assertThat(
+                  client
+                      .configMaps()
+                      .inNamespace(namespace)
+                      .withName(GerritInitConfigMap.getName(gerrit))
+                      .get(),
+                  is(notNullValue()));
+            });
+
+    logger.atInfo().log("Waiting max 1 minutes for the Gerrit StatefulSet to be created.");
+    await()
+        .atMost(1, MINUTES)
+        .untilAsserted(
+            () -> {
+              assertThat(
+                  client
+                      .apps()
+                      .statefulSets()
+                      .inNamespace(namespace)
+                      .withName(gerrit.getMetadata().getName())
+                      .get(),
+                  is(notNullValue()));
+            });
+
+    logger.atInfo().log("Waiting max 1 minutes for the Gerrit Service to be created.");
+    await()
+        .atMost(1, MINUTES)
+        .untilAsserted(
+            () -> {
+              assertThat(
+                  client
+                      .services()
+                      .inNamespace(namespace)
+                      .withName(GerritService.getName(gerrit))
+                      .get(),
+                  is(notNullValue()));
+            });
+
+    logger.atInfo().log("Waiting max 2 minutes for the Gerrit StatefulSet to be ready.");
+    await()
+        .atMost(2, MINUTES)
+        .untilAsserted(
+            () -> {
+              assertTrue(
+                  client
+                      .apps()
+                      .statefulSets()
+                      .inNamespace(namespace)
+                      .withName(gerrit.getMetadata().getName())
+                      .isReady());
+            });
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestGerritCluster.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestGerritCluster.java
new file mode 100644
index 0000000..ce66ec2
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestGerritCluster.java
@@ -0,0 +1,256 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.test;
+
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.k8s.operator.network.IngressType;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritClusterSpec;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplate;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.ReceiverTemplate;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.ContainerImageConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GerritClusterIngressConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GerritIngressTlsConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GerritRepositoryConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GerritStorageConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.NfsWorkaroundConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.SharedStorage;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.StorageClassConfig;
+import com.urswolfer.gerrit.client.rest.GerritAuthData;
+import com.urswolfer.gerrit.client.rest.GerritRestApiFactory;
+import io.fabric8.kubernetes.api.model.LocalObjectReference;
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+import io.fabric8.kubernetes.api.model.Quantity;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+public class TestGerritCluster {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  public static final String CLUSTER_NAME = "test-cluster";
+  public static final TestProperties testProps = new TestProperties();
+
+  private final KubernetesClient client;
+  private final String namespace;
+
+  private GerritClusterIngressConfig ingressConfig;
+  private boolean isNfsEnabled = false;
+  private GerritCluster cluster = new GerritCluster();
+  private String hostname;
+  private List<GerritTemplate> gerrits = new ArrayList<>();
+  private Optional<ReceiverTemplate> receiver = Optional.empty();
+
+  public TestGerritCluster(KubernetesClient client, String namespace) {
+    this.client = client;
+    this.namespace = namespace;
+
+    defaultIngressConfig();
+  }
+
+  public GerritCluster getGerritCluster() {
+    return cluster;
+  }
+
+  public String getHostname() {
+    return hostname;
+  }
+
+  public String getNamespace() {
+    return cluster.getMetadata().getNamespace();
+  }
+
+  public void setIngressType(IngressType type) {
+    switch (type) {
+      case INGRESS:
+        hostname = testProps.getIngressDomain();
+        enableIngress();
+        break;
+      case ISTIO:
+        hostname = testProps.getIstioDomain();
+        enableIngress();
+        break;
+      default:
+        hostname = null;
+        defaultIngressConfig();
+    }
+    deploy();
+  }
+
+  private void defaultIngressConfig() {
+    ingressConfig = new GerritClusterIngressConfig();
+    ingressConfig.setEnabled(false);
+  }
+
+  private void enableIngress() {
+    ingressConfig = new GerritClusterIngressConfig();
+    ingressConfig.setEnabled(true);
+    ingressConfig.setHost(hostname);
+    GerritIngressTlsConfig ingressTlsConfig = new GerritIngressTlsConfig();
+    ingressTlsConfig.setEnabled(true);
+    ingressTlsConfig.setSecret("tls-secret");
+    ingressConfig.setTls(ingressTlsConfig);
+  }
+
+  public GerritApi getGerritApiClient(GerritTemplate gerrit, IngressType ingressType) {
+    return new GerritRestApiFactory()
+        .create(new GerritAuthData.Basic(String.format("https://%s", hostname)));
+  }
+
+  public void setNfsEnabled(boolean isNfsEnabled) {
+    this.isNfsEnabled = isNfsEnabled;
+    deploy();
+  }
+
+  public void addGerrit(GerritTemplate gerrit) {
+    gerrits.add(gerrit);
+  }
+
+  public void removeGerrit(GerritTemplate gerrit) {
+    gerrits.remove(gerrit);
+  }
+
+  public void setReceiver(ReceiverTemplate receiver) {
+    this.receiver = Optional.ofNullable(receiver);
+  }
+
+  public GerritCluster build() {
+    cluster.setMetadata(
+        new ObjectMetaBuilder().withName(CLUSTER_NAME).withNamespace(namespace).build());
+
+    SharedStorage sharedStorage = new SharedStorage();
+    sharedStorage.setSize(Quantity.parse("1Gi"));
+
+    StorageClassConfig storageClassConfig = new StorageClassConfig();
+    storageClassConfig.setReadWriteMany(testProps.getRWMStorageClass());
+
+    NfsWorkaroundConfig nfsWorkaround = new NfsWorkaroundConfig();
+    nfsWorkaround.setEnabled(isNfsEnabled);
+    nfsWorkaround.setIdmapdConfig("[General]\nDomain = localdomain.com");
+    storageClassConfig.setNfsWorkaround(nfsWorkaround);
+
+    GerritClusterSpec clusterSpec = new GerritClusterSpec();
+    GerritStorageConfig gerritStorageConfig = new GerritStorageConfig();
+    gerritStorageConfig.setSharedStorage(sharedStorage);
+    gerritStorageConfig.setStorageClasses(storageClassConfig);
+    clusterSpec.setStorage(gerritStorageConfig);
+
+    GerritRepositoryConfig repoConfig = new GerritRepositoryConfig();
+    repoConfig.setOrg(testProps.getRegistryOrg());
+    repoConfig.setRegistry(testProps.getRegistry());
+    repoConfig.setTag(testProps.getTag());
+
+    ContainerImageConfig containerImageConfig = new ContainerImageConfig();
+    containerImageConfig.setGerritImages(repoConfig);
+    Set<LocalObjectReference> imagePullSecrets = new HashSet<>();
+    imagePullSecrets.add(
+        new LocalObjectReference(AbstractGerritOperatorE2ETest.IMAGE_PULL_SECRET_NAME));
+    containerImageConfig.setImagePullSecrets(imagePullSecrets);
+    clusterSpec.setContainerImages(containerImageConfig);
+
+    clusterSpec.setIngress(ingressConfig);
+
+    clusterSpec.setGerrits(gerrits);
+    if (receiver.isPresent()) {
+      clusterSpec.setReceiver(receiver.get());
+    }
+
+    cluster.setSpec(clusterSpec);
+    return cluster;
+  }
+
+  public void deploy() {
+    build();
+    client.resource(cluster).inNamespace(namespace).createOrReplace();
+    await()
+        .atMost(2, MINUTES)
+        .untilAsserted(
+            () -> {
+              assertThat(
+                  client
+                      .resources(GerritCluster.class)
+                      .inNamespace(namespace)
+                      .withName(CLUSTER_NAME)
+                      .get(),
+                  is(notNullValue()));
+            });
+
+    GerritCluster updatedCluster =
+        client.resources(GerritCluster.class).inNamespace(namespace).withName(CLUSTER_NAME).get();
+    for (GerritTemplate gerrit : updatedCluster.getSpec().getGerrits()) {
+      waitForGerritReadiness(gerrit);
+    }
+    if (receiver.isPresent()) {
+      waitForReceiverReadiness();
+    }
+  }
+
+  private void waitForGerritReadiness(GerritTemplate gerrit) {
+    logger.atInfo().log("Waiting max 2 minutes for the Gerrit StatefulSet to be ready.");
+    await()
+        .pollDelay(15, SECONDS)
+        .atMost(2, MINUTES)
+        .untilAsserted(
+            () -> {
+              assertThat(
+                  client
+                      .apps()
+                      .statefulSets()
+                      .inNamespace(namespace)
+                      .withName(gerrit.getMetadata().getName())
+                      .get(),
+                  is(notNullValue()));
+              assertTrue(
+                  client
+                      .apps()
+                      .statefulSets()
+                      .inNamespace(namespace)
+                      .withName(gerrit.getMetadata().getName())
+                      .isReady());
+              assertTrue(
+                  client
+                      .pods()
+                      .inNamespace(namespace)
+                      .withName(gerrit.getMetadata().getName() + "-0")
+                      .isReady());
+            });
+  }
+
+  private void waitForReceiverReadiness() {
+    await()
+        .atMost(2, MINUTES)
+        .untilAsserted(
+            () -> {
+              assertTrue(
+                  client
+                      .apps()
+                      .deployments()
+                      .inNamespace(namespace)
+                      .withName(receiver.get().getMetadata().getName())
+                      .isReady());
+            });
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestProperties.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestProperties.java
new file mode 100644
index 0000000..d7d23bf
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestProperties.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.test;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.Properties;
+
+public class TestProperties {
+  private final Properties props = getProperties();
+
+  private static Properties getProperties() {
+    String propertiesPath = System.getProperty("properties", "test.properties");
+    Properties props = new Properties();
+    try {
+      props.load(new FileInputStream(propertiesPath));
+    } catch (IOException e) {
+      throw new IllegalStateException("Could not load properties file.");
+    }
+    return props;
+  }
+
+  public String getRWMStorageClass() {
+    return props.getProperty("rwmStorageClass", "nfs-client");
+  }
+
+  public String getRegistry() {
+    return props.getProperty("registry", "");
+  }
+
+  public String getRegistryOrg() {
+    return props.getProperty("registryOrg", "k8sgerrit");
+  }
+
+  public String getRegistryUser() {
+    return props.getProperty("registryUser", "");
+  }
+
+  public String getRegistryPwd() {
+    return props.getProperty("registryPwd", "");
+  }
+
+  public String getTag() {
+    return props.getProperty("tag", "");
+  }
+
+  public String getIngressDomain() {
+    return props.getProperty("ingressDomain", "");
+  }
+
+  public String getIstioDomain() {
+    return props.getProperty("istioDomain", "");
+  }
+
+  public String getLdapAdminPwd() {
+    return props.getProperty("ldapAdminPwd", "");
+  }
+
+  public String getGerritUser() {
+    return props.getProperty("gerritUser", "");
+  }
+
+  public String getGerritPwd() {
+    return props.getProperty("gerritPwd", "");
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestSecureConfig.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestSecureConfig.java
new file mode 100644
index 0000000..04688d7
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestSecureConfig.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.test;
+
+import io.fabric8.kubernetes.api.model.Secret;
+import io.fabric8.kubernetes.api.model.SecretBuilder;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import java.util.Base64;
+import java.util.Map;
+import org.eclipse.jgit.lib.Config;
+
+public class TestSecureConfig {
+  public static final String SECURE_CONFIG_SECRET_NAME = "gerrit-secret";
+
+  private final KubernetesClient client;
+  private final String namespace;
+
+  private Config secureConfig = new Config();
+  private Secret secureConfigSecret;
+
+  public TestSecureConfig(KubernetesClient client, TestProperties testProps, String namespace) {
+    this.client = client;
+    this.namespace = namespace;
+    this.secureConfig.setString("ldap", null, "password", testProps.getLdapAdminPwd());
+  }
+
+  public void createOrReplace() {
+    secureConfigSecret =
+        new SecretBuilder()
+            .withNewMetadata()
+            .withNamespace(namespace)
+            .withName(SECURE_CONFIG_SECRET_NAME)
+            .endMetadata()
+            .withData(
+                Map.of(
+                    "secure.config",
+                    Base64.getEncoder().encodeToString(secureConfig.toText().getBytes())))
+            .build();
+    client.resource(secureConfigSecret).inNamespace(namespace).createOrReplace();
+  }
+
+  public void modify(String section, String key, String value) {
+    secureConfig.setString(section, null, key, value);
+    createOrReplace();
+  }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/host.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/host.yaml
new file mode 100644
index 0000000..59ff0a4
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/host.yaml
@@ -0,0 +1,9 @@
+apiVersion: getambassador.io/v2
+kind: Host
+metadata:
+  name: gerrit-ambassador-host
+spec:
+  ambassador_id:
+    - my-id-1
+    - my-id-2
+  hostname: example.com
\ No newline at end of file
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/host_with_tls.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/host_with_tls.yaml
new file mode 100644
index 0000000..c386b3a
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/host_with_tls.yaml
@@ -0,0 +1,13 @@
+apiVersion: getambassador.io/v2
+kind: Host
+metadata:
+  name: gerrit-ambassador-host
+spec:
+  ambassador_id:
+    - my-id-1
+    - my-id-2
+  hostname: example.com
+  tlsContext:
+    name: gerrit-tls-context
+  tlsSecret:
+    name: tls-secret
\ No newline at end of file
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingGETReplica_primary_replica.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingGETReplica_primary_replica.yaml
new file mode 100644
index 0000000..841d1fb
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingGETReplica_primary_replica.yaml
@@ -0,0 +1,18 @@
+apiVersion: getambassador.io/v2
+kind: Mapping
+metadata:
+  name: gerrit-mapping-get-replica
+  namespace: gerrit
+spec:
+  ambassador_id:
+    - my-id-1
+    - my-id-2
+  bypass_auth: true
+  rewrite: ""
+  host: example.com
+  method: GET
+  prefix: /.*/info/refs
+  prefix_regex: true
+  query_parameters:
+    service: git-upload-pack
+  service: replica:48080
\ No newline at end of file
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingGET_receiver.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingGET_receiver.yaml
new file mode 100644
index 0000000..b179763
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingGET_receiver.yaml
@@ -0,0 +1,18 @@
+apiVersion: getambassador.io/v2
+kind: Mapping
+metadata:
+  name: gerrit-mapping-receiver-get
+  namespace: gerrit
+spec:
+  ambassador_id:
+    - my-id-1
+    - my-id-2
+  bypass_auth: true
+  rewrite: ""
+  host: example.com
+  method: GET
+  prefix: /.*/info/refs
+  prefix_regex: true
+  query_parameters:
+    service: git-receive-pack
+  service: receiver:48081
\ No newline at end of file
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingPOSTReplica_primary_replica.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingPOSTReplica_primary_replica.yaml
new file mode 100644
index 0000000..02f717a
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingPOSTReplica_primary_replica.yaml
@@ -0,0 +1,16 @@
+apiVersion: getambassador.io/v2
+kind: Mapping
+metadata:
+  name: gerrit-mapping-post-replica
+  namespace: gerrit
+spec:
+  ambassador_id:
+    - my-id-1
+    - my-id-2
+  bypass_auth: true
+  rewrite: ""
+  host: example.com
+  method: POST
+  prefix: /.*/git-upload-pack
+  prefix_regex: true
+  service: replica:48080
\ No newline at end of file
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingPrimary_primary_replica.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingPrimary_primary_replica.yaml
new file mode 100644
index 0000000..e6f52a5
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingPrimary_primary_replica.yaml
@@ -0,0 +1,14 @@
+apiVersion: getambassador.io/v2
+kind: Mapping
+metadata:
+  name: gerrit-mapping-primary
+  namespace: gerrit
+spec:
+  ambassador_id:
+    - my-id-1
+    - my-id-2
+  bypass_auth: true
+  rewrite: ""
+  host: example.com
+  prefix: /
+  service: primary:48080
\ No newline at end of file
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mapping_primary.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mapping_primary.yaml
new file mode 100644
index 0000000..4ea6c95
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mapping_primary.yaml
@@ -0,0 +1,14 @@
+apiVersion: getambassador.io/v2
+kind: Mapping
+metadata:
+  name: gerrit-mapping
+  namespace: gerrit
+spec:
+  ambassador_id:
+    - my-id-1
+    - my-id-2
+  bypass_auth: true
+  rewrite: ""
+  host: example.com
+  prefix: /
+  service: primary:48080
\ No newline at end of file
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mapping_receiver.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mapping_receiver.yaml
new file mode 100644
index 0000000..93b9eed
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mapping_receiver.yaml
@@ -0,0 +1,15 @@
+apiVersion: getambassador.io/v2
+kind: Mapping
+metadata:
+  name: gerrit-mapping-receiver
+  namespace: gerrit
+spec:
+  ambassador_id:
+    - my-id-1
+    - my-id-2
+  bypass_auth: true
+  rewrite: ""
+  host: example.com
+  prefix: /a/projects/.*|/.*/git-receive-pack
+  prefix_regex: true
+  service: receiver:48081
\ No newline at end of file
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mapping_replica.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mapping_replica.yaml
new file mode 100644
index 0000000..7fb2b7b
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mapping_replica.yaml
@@ -0,0 +1,14 @@
+apiVersion: getambassador.io/v2
+kind: Mapping
+metadata:
+  name: gerrit-mapping
+  namespace: gerrit
+spec:
+  ambassador_id:
+    - my-id-1
+    - my-id-2
+  bypass_auth: true
+  rewrite: ""
+  host: example.com
+  prefix: /
+  service: replica:48080
\ No newline at end of file
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/tlscontext.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/tlscontext.yaml
new file mode 100644
index 0000000..aee8ddb
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/tlscontext.yaml
@@ -0,0 +1,13 @@
+apiVersion: getambassador.io/v2
+kind: TLSContext
+metadata:
+  name: gerrit-tls-context
+  namespace: gerrit
+spec:
+  ambassador_id:
+    - my-id-1
+    - my-id-2
+  secret: tls-secret
+  secret_namespacing: true
+  hosts:
+    - example.com
\ No newline at end of file
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary.yaml
new file mode 100644
index 0000000..68b81f5
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary.yaml
@@ -0,0 +1,17 @@
+apiVersion: "gerritoperator.google.com/v1alpha1"
+kind: GerritNetwork
+metadata:
+  name: gerrit
+  namespace: gerrit
+spec:
+  ingress:
+    enabled: true
+    host: example.com
+    tls:
+      enabled: false
+    ambassador:
+      id: ["my-id-1", "my-id-2"]
+  primaryGerrit:
+    name: primary
+    httpPort: 48080
+    sshPort: 49418
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica.yaml
new file mode 100644
index 0000000..443e465
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica.yaml
@@ -0,0 +1,21 @@
+apiVersion: "gerritoperator.google.com/v1alpha1"
+kind: GerritNetwork
+metadata:
+  name: gerrit
+  namespace: gerrit
+spec:
+  ingress:
+    enabled: true
+    host: example.com
+    tls:
+      enabled: false
+    ambassador:
+      id: ["my-id-1", "my-id-2"]
+  primaryGerrit:
+    name: primary
+    httpPort: 48080
+    sshPort: 49418
+  gerritReplica:
+    name: replica
+    httpPort: 48080
+    sshPort: 49418
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica_create_host.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica_create_host.yaml
new file mode 100644
index 0000000..ef4f76c
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica_create_host.yaml
@@ -0,0 +1,22 @@
+apiVersion: "gerritoperator.google.com/v1alpha1"
+kind: GerritNetwork
+metadata:
+  name: gerrit
+  namespace: gerrit
+spec:
+  ingress:
+    enabled: true
+    host: example.com
+    tls:
+      enabled: false
+    ambassador:
+      id: ["my-id-1", "my-id-2"]
+      createHost: true
+  primaryGerrit:
+    name: primary
+    httpPort: 48080
+    sshPort: 49418
+  gerritReplica:
+    name: replica
+    httpPort: 48080
+    sshPort: 49418
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica_ssh.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica_ssh.yaml
new file mode 100644
index 0000000..623890f
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica_ssh.yaml
@@ -0,0 +1,21 @@
+apiVersion: "gerritoperator.google.com/v1alpha1"
+kind: GerritNetwork
+metadata:
+  name: gerrit
+  namespace: gerrit
+spec:
+  ingress:
+    enabled: true
+    host: example.com
+    tls:
+      enabled: false
+    ssh:
+      enabled: true
+  primaryGerrit:
+    name: primary
+    httpPort: 48080
+    sshPort: 49418
+  gerritReplica:
+    name: replica
+    httpPort: 48080
+    sshPort: 49419
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica_tls.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica_tls.yaml
new file mode 100644
index 0000000..21b9e59
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica_tls.yaml
@@ -0,0 +1,22 @@
+apiVersion: "gerritoperator.google.com/v1alpha1"
+kind: GerritNetwork
+metadata:
+  name: gerrit
+  namespace: gerrit
+spec:
+  ingress:
+    enabled: true
+    host: example.com
+    tls:
+      enabled: true
+      secret: tls-secret
+    ambassador:
+      id: ["my-id-1", "my-id-2"]
+  primaryGerrit:
+    name: primary
+    httpPort: 48080
+    sshPort: 49418
+  gerritReplica:
+    name: replica
+    httpPort: 48080
+    sshPort: 49418
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica_tls_create_host.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica_tls_create_host.yaml
new file mode 100644
index 0000000..c84e3cc
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica_tls_create_host.yaml
@@ -0,0 +1,23 @@
+apiVersion: "gerritoperator.google.com/v1alpha1"
+kind: GerritNetwork
+metadata:
+  name: gerrit
+  namespace: gerrit
+spec:
+  ingress:
+    enabled: true
+    host: example.com
+    tls:
+      enabled: true
+      secret: tls-secret
+    ambassador:
+      id: ["my-id-1", "my-id-2"]
+      createHost: true
+  primaryGerrit:
+    name: primary
+    httpPort: 48080
+    sshPort: 49418
+  gerritReplica:
+    name: replica
+    httpPort: 48080
+    sshPort: 49418
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_ssh.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_ssh.yaml
new file mode 100644
index 0000000..a6b3271
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_ssh.yaml
@@ -0,0 +1,17 @@
+apiVersion: "gerritoperator.google.com/v1alpha1"
+kind: GerritNetwork
+metadata:
+  name: gerrit
+  namespace: gerrit
+spec:
+  ingress:
+    enabled: true
+    host: example.com
+    tls:
+      enabled: false
+    ssh:
+      enabled: true
+  primaryGerrit:
+    name: primary
+    httpPort: 48080
+    sshPort: 49418
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_receiver_replica.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_receiver_replica.yaml
new file mode 100644
index 0000000..e6da865
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_receiver_replica.yaml
@@ -0,0 +1,20 @@
+apiVersion: "gerritoperator.google.com/v1alpha1"
+kind: GerritNetwork
+metadata:
+  name: gerrit
+  namespace: gerrit
+spec:
+  ingress:
+    enabled: true
+    host: example.com
+    tls:
+      enabled: false
+    ambassador:
+      id: ["my-id-1", "my-id-2"]
+  gerritReplica:
+    name: replica
+    httpPort: 48080
+    sshPort: 49418
+  receiver:
+    name: receiver
+    httpPort: 48081
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_receiver_replica_ssh.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_receiver_replica_ssh.yaml
new file mode 100644
index 0000000..7efc649
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_receiver_replica_ssh.yaml
@@ -0,0 +1,20 @@
+apiVersion: "gerritoperator.google.com/v1alpha1"
+kind: GerritNetwork
+metadata:
+  name: gerrit
+  namespace: gerrit
+spec:
+  ingress:
+    enabled: true
+    host: example.com
+    tls:
+      enabled: false
+    ssh:
+      enabled: true
+  gerritReplica:
+    name: replica
+    httpPort: 48080
+    sshPort: 49419
+  receiver:
+    name: receiver
+    httpPort: 48081
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_receiver_replica_tls.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_receiver_replica_tls.yaml
new file mode 100644
index 0000000..d5bd3c0
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_receiver_replica_tls.yaml
@@ -0,0 +1,21 @@
+apiVersion: "gerritoperator.google.com/v1alpha1"
+kind: GerritNetwork
+metadata:
+  name: gerrit
+  namespace: gerrit
+spec:
+  ingress:
+    enabled: true
+    host: example.com
+    tls:
+      enabled: true
+      secret: tls-secret
+    ambassador:
+      id: ["my-id-1", "my-id-2"]
+  gerritReplica:
+    name: replica
+    httpPort: 48080
+    sshPort: 49418
+  receiver:
+    name: receiver
+    httpPort: 48081
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_replica.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_replica.yaml
new file mode 100644
index 0000000..915da97
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_replica.yaml
@@ -0,0 +1,17 @@
+apiVersion: "gerritoperator.google.com/v1alpha1"
+kind: GerritNetwork
+metadata:
+  name: gerrit
+  namespace: gerrit
+spec:
+  ingress:
+    enabled: true
+    host: example.com
+    tls:
+      enabled: false
+    ambassador:
+      id: ["my-id-1", "my-id-2"]
+  gerritReplica:
+    name: replica
+    httpPort: 48080
+    sshPort: 49418
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_replica_ssh.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_replica_ssh.yaml
new file mode 100644
index 0000000..efa54a7
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_replica_ssh.yaml
@@ -0,0 +1,17 @@
+apiVersion: "gerritoperator.google.com/v1alpha1"
+kind: GerritNetwork
+metadata:
+  name: gerrit
+  namespace: gerrit
+spec:
+  ingress:
+    enabled: true
+    host: example.com
+    tls:
+      enabled: false
+    ssh:
+      enabled: true
+  gerritReplica:
+    name: replica
+    httpPort: 48080
+    sshPort: 49419
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_primary.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_primary.yaml
new file mode 100644
index 0000000..9b18d5c
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_primary.yaml
@@ -0,0 +1,25 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: gerrit-ingress
+  namespace: gerrit
+  annotations:
+    nginx.ingress.kubernetes.io/use-regex: true
+    kubernetes.io/ingress.class: nginx
+    nginx.ingress.kubernetes.io/affinity: cookie
+    nginx.ingress.kubernetes.io/session-cookie-name: Gerrit_Session
+    nginx.ingress.kubernetes.io/session-cookie-path: /
+    nginx.ingress.kubernetes.io/session-cookie-max-age: 60
+    nginx.ingress.kubernetes.io/session-cookie-expires: 60
+spec:
+  rules:
+  - host: example.com
+    http:
+      paths:
+      - pathType: Prefix
+        path: "/"
+        backend:
+          service:
+            name: primary
+            port:
+              name: http
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_primary_replica.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_primary_replica.yaml
new file mode 100644
index 0000000..d6aae78
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_primary_replica.yaml
@@ -0,0 +1,38 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: gerrit-ingress
+  namespace: gerrit
+  annotations:
+    nginx.ingress.kubernetes.io/use-regex: true
+    kubernetes.io/ingress.class: nginx
+    nginx.ingress.kubernetes.io/configuration-snippet: |-
+      if ($args ~ service=git-upload-pack){
+        set $proxy_upstream_name "gerrit-replica-http";
+        set $proxy_host $proxy_upstream_name;
+        set $service_name "replica";
+      }
+    nginx.ingress.kubernetes.io/affinity: cookie
+    nginx.ingress.kubernetes.io/session-cookie-name: Gerrit_Session
+    nginx.ingress.kubernetes.io/session-cookie-path: /
+    nginx.ingress.kubernetes.io/session-cookie-max-age: 60
+    nginx.ingress.kubernetes.io/session-cookie-expires: 60
+spec:
+  rules:
+  - host: example.com
+    http:
+      paths:
+      - pathType: Prefix
+        path: "/.*/git-upload-pack"
+        backend:
+          service:
+            name: replica
+            port:
+              name: http
+      - pathType: Prefix
+        path: "/"
+        backend:
+          service:
+            name: primary
+            port:
+              name: http
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_primary_replica_tls.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_primary_replica_tls.yaml
new file mode 100644
index 0000000..adbe808
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_primary_replica_tls.yaml
@@ -0,0 +1,42 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: gerrit-ingress
+  namespace: gerrit
+  annotations:
+    nginx.ingress.kubernetes.io/use-regex: true
+    kubernetes.io/ingress.class: nginx
+    nginx.ingress.kubernetes.io/configuration-snippet: |-
+      if ($args ~ service=git-upload-pack){
+        set $proxy_upstream_name "gerrit-replica-http";
+        set $proxy_host $proxy_upstream_name;
+        set $service_name "replica";
+      }
+    nginx.ingress.kubernetes.io/affinity: cookie
+    nginx.ingress.kubernetes.io/session-cookie-name: Gerrit_Session
+    nginx.ingress.kubernetes.io/session-cookie-path: /
+    nginx.ingress.kubernetes.io/session-cookie-max-age: 60
+    nginx.ingress.kubernetes.io/session-cookie-expires: 60
+spec:
+  tls:
+  - hosts:
+    - example.com
+    secretName: tls-secret
+  rules:
+  - host: example.com
+    http:
+      paths:
+      - pathType: Prefix
+        path: "/.*/git-upload-pack"
+        backend:
+          service:
+            name: replica
+            port:
+              name: http
+      - pathType: Prefix
+        path: "/"
+        backend:
+          service:
+            name: primary
+            port:
+              name: http
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_receiver_replica.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_receiver_replica.yaml
new file mode 100644
index 0000000..75a37c7
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_receiver_replica.yaml
@@ -0,0 +1,45 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: gerrit-ingress
+  namespace: gerrit
+  annotations:
+    nginx.ingress.kubernetes.io/use-regex: true
+    kubernetes.io/ingress.class: nginx
+    nginx.ingress.kubernetes.io/affinity: cookie
+    nginx.ingress.kubernetes.io/session-cookie-name: Gerrit_Session
+    nginx.ingress.kubernetes.io/session-cookie-path: /
+    nginx.ingress.kubernetes.io/session-cookie-max-age: 60
+    nginx.ingress.kubernetes.io/session-cookie-expires: 60
+    nginx.ingress.kubernetes.io/configuration-snippet: |-
+      if ($args ~ service=git-receive-pack){
+        set $proxy_upstream_name "gerrit-receiver-http";
+        set $proxy_host $proxy_upstream_name;
+        set $service_name "receiver";
+      }
+spec:
+  rules:
+  - host: example.com
+    http:
+      paths:
+      - pathType: Prefix
+        path: "/a/projects"
+        backend:
+          service:
+            name: receiver
+            port:
+              name: http
+      - pathType: Prefix
+        path: "/.*/git-receive-pack"
+        backend:
+          service:
+            name: receiver
+            port:
+              name: http
+      - pathType: Prefix
+        path: "/"
+        backend:
+          service:
+            name: replica
+            port:
+              name: http
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_receiver_replica_tls.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_receiver_replica_tls.yaml
new file mode 100644
index 0000000..ad4cfbf
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_receiver_replica_tls.yaml
@@ -0,0 +1,49 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: gerrit-ingress
+  namespace: gerrit
+  annotations:
+    nginx.ingress.kubernetes.io/use-regex: true
+    kubernetes.io/ingress.class: nginx
+    nginx.ingress.kubernetes.io/affinity: cookie
+    nginx.ingress.kubernetes.io/session-cookie-name: Gerrit_Session
+    nginx.ingress.kubernetes.io/session-cookie-path: /
+    nginx.ingress.kubernetes.io/session-cookie-max-age: 60
+    nginx.ingress.kubernetes.io/session-cookie-expires: 60
+    nginx.ingress.kubernetes.io/configuration-snippet: |-
+      if ($args ~ service=git-receive-pack){
+        set $proxy_upstream_name "gerrit-receiver-http";
+        set $proxy_host $proxy_upstream_name;
+        set $service_name "receiver";
+      }
+spec:
+  tls:
+  - hosts:
+    - example.com
+    secretName: tls-secret
+  rules:
+  - host: example.com
+    http:
+      paths:
+      - pathType: Prefix
+        path: "/a/projects"
+        backend:
+          service:
+            name: receiver
+            port:
+              name: http
+      - pathType: Prefix
+        path: "/.*/git-receive-pack"
+        backend:
+          service:
+            name: receiver
+            port:
+              name: http
+      - pathType: Prefix
+        path: "/"
+        backend:
+          service:
+            name: replica
+            port:
+              name: http
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_replica.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_replica.yaml
new file mode 100644
index 0000000..07dfe7d
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_replica.yaml
@@ -0,0 +1,25 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: gerrit-ingress
+  namespace: gerrit
+  annotations:
+    nginx.ingress.kubernetes.io/use-regex: true
+    kubernetes.io/ingress.class: nginx
+    nginx.ingress.kubernetes.io/affinity: cookie
+    nginx.ingress.kubernetes.io/session-cookie-name: Gerrit_Session
+    nginx.ingress.kubernetes.io/session-cookie-path: /
+    nginx.ingress.kubernetes.io/session-cookie-max-age: 60
+    nginx.ingress.kubernetes.io/session-cookie-expires: 60
+spec:
+  rules:
+  - host: example.com
+    http:
+      paths:
+      - pathType: Prefix
+        path: "/"
+        backend:
+          service:
+            name: replica
+            port:
+              name: http
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway.yaml
new file mode 100644
index 0000000..ccbab63
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway.yaml
@@ -0,0 +1,17 @@
+apiVersion: networking.istio.io/v1beta1
+kind: Gateway
+metadata:
+  name: gerrit-istio-gateway
+  namespace: gerrit
+spec:
+  selector:
+    istio: ingressgateway
+  servers:
+  - port:
+      number: 80
+      name: http
+      protocol: HTTP
+    hosts:
+    - example.com
+    tls:
+      httpsRedirect: false
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_primary_replica_ssh.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_primary_replica_ssh.yaml
new file mode 100644
index 0000000..70a38f2
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_primary_replica_ssh.yaml
@@ -0,0 +1,29 @@
+apiVersion: networking.istio.io/v1beta1
+kind: Gateway
+metadata:
+  name: gerrit-istio-gateway
+  namespace: gerrit
+spec:
+  selector:
+    istio: ingressgateway
+  servers:
+  - port:
+      number: 80
+      name: http
+      protocol: HTTP
+    hosts:
+    - example.com
+    tls:
+      httpsRedirect: false
+  - port:
+      number: 49418
+      name: ssh-primary
+      protocol: TCP
+    hosts:
+    - example.com
+  - port:
+      number: 49419
+      name: ssh-replica
+      protocol: TCP
+    hosts:
+    - example.com
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_primary_ssh.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_primary_ssh.yaml
new file mode 100644
index 0000000..7815aba
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_primary_ssh.yaml
@@ -0,0 +1,23 @@
+apiVersion: networking.istio.io/v1beta1
+kind: Gateway
+metadata:
+  name: gerrit-istio-gateway
+  namespace: gerrit
+spec:
+  selector:
+    istio: ingressgateway
+  servers:
+  - port:
+      number: 80
+      name: http
+      protocol: HTTP
+    hosts:
+    - example.com
+    tls:
+      httpsRedirect: false
+  - port:
+      number: 49418
+      name: ssh-primary
+      protocol: TCP
+    hosts:
+    - example.com
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_receiver_replica_ssh.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_receiver_replica_ssh.yaml
new file mode 100644
index 0000000..04212e3
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_receiver_replica_ssh.yaml
@@ -0,0 +1,23 @@
+apiVersion: networking.istio.io/v1beta1
+kind: Gateway
+metadata:
+  name: gerrit-istio-gateway
+  namespace: gerrit
+spec:
+  selector:
+    istio: ingressgateway
+  servers:
+  - port:
+      number: 80
+      name: http
+      protocol: HTTP
+    hosts:
+    - example.com
+    tls:
+      httpsRedirect: false
+  - port:
+      number: 49419
+      name: ssh-replica
+      protocol: TCP
+    hosts:
+    - example.com
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_replica_ssh.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_replica_ssh.yaml
new file mode 100644
index 0000000..04212e3
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_replica_ssh.yaml
@@ -0,0 +1,23 @@
+apiVersion: networking.istio.io/v1beta1
+kind: Gateway
+metadata:
+  name: gerrit-istio-gateway
+  namespace: gerrit
+spec:
+  selector:
+    istio: ingressgateway
+  servers:
+  - port:
+      number: 80
+      name: http
+      protocol: HTTP
+    hosts:
+    - example.com
+    tls:
+      httpsRedirect: false
+  - port:
+      number: 49419
+      name: ssh-replica
+      protocol: TCP
+    hosts:
+    - example.com
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_tls.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_tls.yaml
new file mode 100644
index 0000000..9f64ad5
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_tls.yaml
@@ -0,0 +1,26 @@
+apiVersion: networking.istio.io/v1beta1
+kind: Gateway
+metadata:
+  name: gerrit-istio-gateway
+  namespace: gerrit
+spec:
+  selector:
+    istio: ingressgateway
+  servers:
+  - port:
+      number: 80
+      name: http
+      protocol: HTTP
+    hosts:
+    - example.com
+    tls:
+      httpsRedirect: true
+  - port:
+      number: 443
+      name: https
+      protocol: HTTPS
+    hosts:
+    - example.com
+    tls:
+      mode: SIMPLE
+      credentialName: tls-secret
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_primary.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_primary.yaml
new file mode 100644
index 0000000..ab1bf7a
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_primary.yaml
@@ -0,0 +1,17 @@
+apiVersion: networking.istio.io/v1beta1
+kind: VirtualService
+metadata:
+  name: gerrit-gerrit-http-virtual-service
+  namespace: gerrit
+spec:
+  hosts:
+  - example.com
+  gateways:
+  - gerrit/gerrit-istio-gateway
+  http:
+  - name: gerrit-primary-primary
+    route:
+    - destination:
+        port:
+          number: 48080
+        host: primary.gerrit.svc.cluster.local
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_primary_replica.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_primary_replica.yaml
new file mode 100644
index 0000000..f97524d
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_primary_replica.yaml
@@ -0,0 +1,37 @@
+apiVersion: networking.istio.io/v1beta1
+kind: VirtualService
+metadata:
+  name: gerrit-gerrit-http-virtual-service
+  namespace: gerrit
+spec:
+  hosts:
+  - example.com
+  gateways:
+  - gerrit/gerrit-istio-gateway
+  http:
+  - name: gerrit-replica-replica
+    match:
+    - uri:
+        regex: "^/(.*)/info/refs$"
+      queryParams:
+        service:
+          exact: git-upload-pack
+      ignoreUriCase: true
+      method:
+        exact: GET
+    - uri:
+        regex: "^/(.*)/git-upload-pack$"
+      ignoreUriCase: true
+      method:
+        exact: POST
+    route:
+    - destination:
+        port:
+          number: 48080
+        host: replica.gerrit.svc.cluster.local
+  - name: gerrit-primary-primary
+    route:
+    - destination:
+        port:
+          number: 48080
+        host: primary.gerrit.svc.cluster.local
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_primary_replica_ssh.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_primary_replica_ssh.yaml
new file mode 100644
index 0000000..30ffb44
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_primary_replica_ssh.yaml
@@ -0,0 +1,52 @@
+apiVersion: networking.istio.io/v1beta1
+kind: VirtualService
+metadata:
+  name: gerrit-gerrit-http-virtual-service
+  namespace: gerrit
+spec:
+  hosts:
+  - example.com
+  gateways:
+  - gerrit/gerrit-istio-gateway
+  http:
+  - name: gerrit-replica-replica
+    match:
+    - uri:
+        regex: "^/(.*)/info/refs$"
+      queryParams:
+        service:
+          exact: git-upload-pack
+      ignoreUriCase: true
+      method:
+        exact: GET
+    - uri:
+        regex: "^/(.*)/git-upload-pack$"
+      ignoreUriCase: true
+      method:
+        exact: POST
+    route:
+    - destination:
+        port:
+          number: 48080
+        host: replica.gerrit.svc.cluster.local
+  - name: gerrit-primary-primary
+    route:
+    - destination:
+        port:
+          number: 48080
+        host: primary.gerrit.svc.cluster.local
+  tcp:
+  - match:
+    - port: 49418
+    route:
+    - destination:
+        port:
+          number: 49418
+        host: primary.gerrit.svc.cluster.local
+  - match:
+    - port: 49419
+    route:
+    - destination:
+        port:
+          number: 49419
+        host: replica.gerrit.svc.cluster.local
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_primary_ssh.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_primary_ssh.yaml
new file mode 100644
index 0000000..73f5bd7
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_primary_ssh.yaml
@@ -0,0 +1,25 @@
+apiVersion: networking.istio.io/v1beta1
+kind: VirtualService
+metadata:
+  name: gerrit-gerrit-http-virtual-service
+  namespace: gerrit
+spec:
+  hosts:
+  - example.com
+  gateways:
+  - gerrit/gerrit-istio-gateway
+  http:
+  - name: gerrit-primary-primary
+    route:
+    - destination:
+        port:
+          number: 48080
+        host: primary.gerrit.svc.cluster.local
+  tcp:
+  - match:
+    - port: 49418
+    route:
+    - destination:
+        port:
+          number: 49418
+        host: primary.gerrit.svc.cluster.local
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_receiver_replica.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_receiver_replica.yaml
new file mode 100644
index 0000000..3a224d6
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_receiver_replica.yaml
@@ -0,0 +1,33 @@
+apiVersion: networking.istio.io/v1beta1
+kind: VirtualService
+metadata:
+  name: gerrit-gerrit-http-virtual-service
+  namespace: gerrit
+spec:
+  hosts:
+  - example.com
+  gateways:
+  - gerrit/gerrit-istio-gateway
+  http:
+  - name: receiver-receiver
+    match:
+    - uri:
+        prefix: "/a/projects/"
+    - uri:
+        regex: "^/(.*)/git-receive-pack$"
+    - uri:
+        regex: "^/(.*)/info/refs$"
+      queryParams:
+        service:
+          exact: git-receive-pack
+    route:
+    - destination:
+        port:
+          number: 48081
+        host: receiver.gerrit.svc.cluster.local
+  - name: gerrit-replica-replica
+    route:
+    - destination:
+        port:
+          number: 48080
+        host: replica.gerrit.svc.cluster.local
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_receiver_replica_ssh.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_receiver_replica_ssh.yaml
new file mode 100644
index 0000000..422e2e9
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_receiver_replica_ssh.yaml
@@ -0,0 +1,41 @@
+apiVersion: networking.istio.io/v1beta1
+kind: VirtualService
+metadata:
+  name: gerrit-gerrit-http-virtual-service
+  namespace: gerrit
+spec:
+  hosts:
+  - example.com
+  gateways:
+  - gerrit/gerrit-istio-gateway
+  http:
+  - name: receiver-receiver
+    match:
+    - uri:
+        prefix: "/a/projects/"
+    - uri:
+        regex: "^/(.*)/git-receive-pack$"
+    - uri:
+        regex: "^/(.*)/info/refs$"
+      queryParams:
+        service:
+          exact: git-receive-pack
+    route:
+    - destination:
+        port:
+          number: 48081
+        host: receiver.gerrit.svc.cluster.local
+  - name: gerrit-replica-replica
+    route:
+    - destination:
+        port:
+          number: 48080
+        host: replica.gerrit.svc.cluster.local
+  tcp:
+  - match:
+    - port: 49419
+    route:
+    - destination:
+        port:
+          number: 49419
+        host: replica.gerrit.svc.cluster.local
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_replica.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_replica.yaml
new file mode 100644
index 0000000..d077def
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_replica.yaml
@@ -0,0 +1,17 @@
+apiVersion: networking.istio.io/v1beta1
+kind: VirtualService
+metadata:
+  name: gerrit-gerrit-http-virtual-service
+  namespace: gerrit
+spec:
+  hosts:
+  - example.com
+  gateways:
+  - gerrit/gerrit-istio-gateway
+  http:
+  - name: gerrit-replica-replica
+    route:
+    - destination:
+        port:
+          number: 48080
+        host: replica.gerrit.svc.cluster.local
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_replica_ssh.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_replica_ssh.yaml
new file mode 100644
index 0000000..d9e7fd3
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_replica_ssh.yaml
@@ -0,0 +1,25 @@
+apiVersion: networking.istio.io/v1beta1
+kind: VirtualService
+metadata:
+  name: gerrit-gerrit-http-virtual-service
+  namespace: gerrit
+spec:
+  hosts:
+  - example.com
+  gateways:
+  - gerrit/gerrit-istio-gateway
+  http:
+  - name: gerrit-replica-replica
+    route:
+    - destination:
+        port:
+          number: 48080
+        host: replica.gerrit.svc.cluster.local
+  tcp:
+  - match:
+    - port: 49419
+    route:
+    - destination:
+        port:
+          number: 49419
+        host: replica.gerrit.svc.cluster.local
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/dependent/deployment.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/dependent/deployment.yaml
new file mode 100644
index 0000000..4857152
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/dependent/deployment.yaml
@@ -0,0 +1,118 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: receiver
+  namespace: gerrit
+  labels:
+    app.kubernetes.io/managed-by: gerrit-operator
+    app.kubernetes.io/name: gerrit
+    app.kubernetes.io/part-of: receiver
+    app.kubernetes.io/created-by: ReceiverReconciler
+    app.kubernetes.io/instance: receiver
+    app.kubernetes.io/version: unknown
+    app.kubernetes.io/component: receiver-deployment-receiver
+spec:
+  replicas: 1
+  strategy:
+    rollingUpdate:
+      maxSurge: 1
+      maxUnavailable: 1
+  selector:
+    matchLabels:
+      app.kubernetes.io/managed-by: gerrit-operator
+      app.kubernetes.io/name: gerrit
+      app.kubernetes.io/part-of: receiver
+      app.kubernetes.io/instance: receiver
+      app.kubernetes.io/component: receiver-deployment-receiver
+  template:
+    metadata:
+      labels:
+        app.kubernetes.io/managed-by: gerrit-operator
+        app.kubernetes.io/name: gerrit
+        app.kubernetes.io/part-of: receiver
+        app.kubernetes.io/created-by: ReceiverReconciler
+        app.kubernetes.io/instance: receiver
+        app.kubernetes.io/version: unknown
+        app.kubernetes.io/component: receiver-deployment-receiver
+    spec:
+      tolerations:
+      - key: key1
+        operator: Equal
+        value: value1
+        effect: NoSchedule
+      topologySpreadConstraints:
+      - maxSkew: 1
+        topologyKey: zone
+        whenUnsatisfiable: DoNotSchedule
+        labelSelector:
+          matchLabels:
+            foo: bar
+      affinity:
+        nodeAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+            nodeSelectorTerms:
+            - matchExpressions:
+              - key: disktype
+                operator: In
+                values:
+                - ssd
+      priorityClassName: prio
+      securityContext:
+        fsGroup: 100
+      imagePullSecrets: []
+      initContainers: []
+      containers:
+      - name: apache-git-http-backend
+        imagePullPolicy: Always
+        image: docker.io/k8sgerrit/apache-git-http-backend:latest
+        env:
+        - name: POD_NAME
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.name
+        ports:
+        - name: http
+          containerPort: 80
+        resources:
+          requests:
+            cpu: 1
+            memory: 5Gi
+          limits:
+            cpu: 1
+            memory: 6Gi
+
+        readinessProbe:
+          tcpSocket:
+            port: 80
+          initialDelaySeconds: 0
+          periodSeconds: 10
+          timeoutSeconds: 1
+          successThreshold: 1
+          failureThreshold: 3
+
+        livenessProbe:
+          tcpSocket:
+            port: 80
+          initialDelaySeconds: 0
+          periodSeconds: 10
+          timeoutSeconds: 1
+          successThreshold: 1
+          failureThreshold: 3
+
+        volumeMounts:
+        - name: shared
+          subPathExpr: "logs/$(POD_NAME)"
+          mountPath: /var/log/apache2
+        - name: apache-credentials
+          mountPath: /var/apache/credentials/.htpasswd
+          subPath: .htpasswd
+        - name: shared
+          subPath: git
+          mountPath: /var/gerrit/git
+      volumes:
+      - name: shared
+        persistentVolumeClaim:
+          claimName: shared-pvc
+      - name: apache-credentials
+        secret:
+          secretName: apache-credentials
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/dependent/deployment_minimal.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/dependent/deployment_minimal.yaml
new file mode 100644
index 0000000..92d0752
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/dependent/deployment_minimal.yaml
@@ -0,0 +1,79 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: receiver
+  namespace: gerrit
+  labels:
+    app.kubernetes.io/managed-by: gerrit-operator
+    app.kubernetes.io/name: gerrit
+    app.kubernetes.io/part-of: receiver
+    app.kubernetes.io/created-by: ReceiverReconciler
+    app.kubernetes.io/instance: receiver
+    app.kubernetes.io/version: unknown
+    app.kubernetes.io/component: receiver-deployment-receiver
+spec:
+  replicas: 1
+  strategy:
+    rollingUpdate:
+      maxSurge: 1
+      maxUnavailable: 1
+  selector:
+    matchLabels:
+      app.kubernetes.io/managed-by: gerrit-operator
+      app.kubernetes.io/name: gerrit
+      app.kubernetes.io/part-of: receiver
+      app.kubernetes.io/instance: receiver
+      app.kubernetes.io/component: receiver-deployment-receiver
+  template:
+    metadata:
+      labels:
+        app.kubernetes.io/managed-by: gerrit-operator
+        app.kubernetes.io/name: gerrit
+        app.kubernetes.io/part-of: receiver
+        app.kubernetes.io/created-by: ReceiverReconciler
+        app.kubernetes.io/instance: receiver
+        app.kubernetes.io/version: unknown
+        app.kubernetes.io/component: receiver-deployment-receiver
+    spec:
+      securityContext:
+        fsGroup: 100
+      imagePullSecrets: []
+      initContainers: []
+      containers:
+      - name: apache-git-http-backend
+        imagePullPolicy: Always
+        image: docker.io/k8sgerrit/apache-git-http-backend:latest
+        env:
+        - name: POD_NAME
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.name
+        ports:
+        - name: http
+          containerPort: 80
+
+        readinessProbe:
+          tcpSocket:
+            port: 80
+
+        livenessProbe:
+          tcpSocket:
+            port: 80
+
+        volumeMounts:
+        - name: shared
+          subPathExpr: "logs/$(POD_NAME)"
+          mountPath: /var/log/apache2
+        - name: apache-credentials
+          mountPath: /var/apache/credentials/.htpasswd
+          subPath: .htpasswd
+        - name: shared
+          subPath: git
+          mountPath: /var/gerrit/git
+      volumes:
+      - name: shared
+        persistentVolumeClaim:
+          claimName: shared-pvc
+      - name: apache-credentials
+        secret:
+          secretName: apache-credentials
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/dependent/service.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/dependent/service.yaml
new file mode 100644
index 0000000..907ee1e
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/dependent/service.yaml
@@ -0,0 +1,25 @@
+apiVersion: v1
+kind: Service
+metadata:
+  name: receiver
+  namespace: gerrit
+  labels:
+    app.kubernetes.io/managed-by: gerrit-operator
+    app.kubernetes.io/name: gerrit
+    app.kubernetes.io/part-of: receiver
+    app.kubernetes.io/created-by: ReceiverReconciler
+    app.kubernetes.io/instance: receiver
+    app.kubernetes.io/version: unknown
+    app.kubernetes.io/component: receiver-service
+spec:
+  type: NodePort
+  ports:
+  - name: http
+    port: 80
+    targetPort: 80
+  selector:
+    app.kubernetes.io/managed-by: gerrit-operator
+    app.kubernetes.io/name: gerrit
+    app.kubernetes.io/part-of: receiver
+    app.kubernetes.io/instance: receiver
+    app.kubernetes.io/component: receiver-deployment-receiver
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/receiver.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/receiver.yaml
new file mode 100644
index 0000000..3364ba0
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/receiver.yaml
@@ -0,0 +1,105 @@
+apiVersion: "gerritoperator.google.com/v1alpha6"
+kind: Receiver
+metadata:
+  name: receiver
+  namespace: gerrit
+spec:
+  tolerations:
+  - key: key1
+    operator: Equal
+    value: value1
+    effect: NoSchedule
+
+  affinity:
+    nodeAffinity:
+      requiredDuringSchedulingIgnoredDuringExecution:
+        nodeSelectorTerms:
+        - matchExpressions:
+          - key: disktype
+            operator: In
+            values:
+            - ssd
+
+  topologySpreadConstraints:
+  - maxSkew: 1
+    topologyKey: zone
+    whenUnsatisfiable: DoNotSchedule
+    labelSelector:
+      matchLabels:
+        foo: bar
+
+  priorityClassName: "prio"
+
+  replicas: 1
+  maxSurge: 1
+  maxUnavailable: 1
+
+  resources:
+    requests:
+      cpu: 1
+      memory: 5Gi
+    limits:
+      cpu: 1
+      memory: 6Gi
+
+  readinessProbe:
+    initialDelaySeconds: 0
+    periodSeconds: 10
+    timeoutSeconds: 1
+    successThreshold: 1
+    failureThreshold: 3
+
+  livenessProbe:
+    initialDelaySeconds: 0
+    periodSeconds: 10
+    timeoutSeconds: 1
+    successThreshold: 1
+    failureThreshold: 3
+
+  service:
+    type: NodePort
+    httpPort: 80
+
+  credentialSecretRef: apache-credentials
+
+  containerImages:
+    imagePullSecrets: []
+    imagePullPolicy: Always
+    gerritImages:
+      registry: docker.io
+      org: k8sgerrit
+      tag: latest
+    busyBox:
+      registry: docker.io
+      tag: latest
+
+  storage:
+    storageClasses:
+      readWriteOnce: default
+      readWriteMany: shared-storage
+      nfsWorkaround:
+        enabled: false
+        chownOnStartup: false
+        idmapdConfig: |-
+          [General]
+            Verbosity = 0
+            Domain = localdomain.com
+
+          [Mapping]
+            Nobody-User = nobody
+            Nobody-Group = nogroup
+
+    sharedStorage:
+      externalPVC:
+        enabled: false
+        claimName: ""
+      size: 1Gi
+      volumeName: ""
+      selector:
+        matchLabels:
+          volume-type: ssd
+          aws-availability-zone: us-east-1
+
+  ingress:
+    host: example.com
+    tlsEnabled: false
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/receiver_minimal.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/receiver_minimal.yaml
new file mode 100644
index 0000000..8fdf155
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/receiver_minimal.yaml
@@ -0,0 +1,15 @@
+apiVersion: "gerritoperator.google.com/v1alpha6"
+kind: Receiver
+metadata:
+  name: receiver
+  namespace: gerrit
+spec:
+  credentialSecretRef: apache-credentials
+
+  storage:
+    storageClasses:
+      readWriteOnce: default
+      readWriteMany: shared-storage
+
+    sharedStorage:
+      size: 1Gi
diff --git a/charts/k8s-gerrit/operator/test.properties b/charts/k8s-gerrit/operator/test.properties
new file mode 100644
index 0000000..b6d5cdc
--- /dev/null
+++ b/charts/k8s-gerrit/operator/test.properties
@@ -0,0 +1,18 @@
+# Storage
+rwmStorageClass=
+
+# Container registry
+registry=
+registryOrg=
+tag=
+registryUser=
+registryPwd=
+
+# Ingress
+ingressDomain=
+istioDomain=
+
+# LDAP
+ldapAdminPwd=
+gerritUser=
+gerritPwd=
diff --git a/charts/k8s-gerrit/publish b/charts/k8s-gerrit/publish
new file mode 100755
index 0000000..ba8d368
--- /dev/null
+++ b/charts/k8s-gerrit/publish
@@ -0,0 +1,80 @@
+#!/bin/bash
+
+usage() {
+    me=`basename "$0"`
+    echo >&2 "Usage: $me [--help] [--update-latest] [--registry REGISTRY] [--org ORGANIZATION] [--no-push] [--tag TAG] [IMAGE]"
+    exit 1
+}
+
+UPDATE_LATEST=false
+ORGANIZATION=k8sgerrit
+PUSH_IMAGES=true
+
+while test $# -gt 0 ; do
+  case "$1" in
+  --help)
+    usage
+    ;;
+  --update-latest)
+    UPDATE_LATEST=true
+    shift
+    ;;
+  --registry)
+    shift
+    REGISTRY=$1
+    shift
+    ;;
+  --org)
+    shift
+    ORGANIZATION=$1
+    shift
+    ;;
+  --no-push)
+    PUSH_IMAGES=false
+    shift
+    ;;
+  --tag)
+    shift
+    TAG=$1
+    shift
+    ;;
+  *)
+    break
+  esac
+done
+
+if test -z "$TAG"; then
+  TAG=$(./get_version.sh)
+fi
+
+#Get list of images
+source container-images/publish_list
+IMAGES=$(get_image_list)
+
+test -n "$REGISTRY" && [[ "$REGISTRY" != */ ]] && REGISTRY="$REGISTRY/"
+
+publish_image(){
+  IMAGE=$1
+  if test "$UPDATE_LATEST" = "true" ; then
+    docker image tag k8sgerrit/$IMAGE:$TAG ${REGISTRY}${ORGANIZATION}/$IMAGE:latest
+    if test "$PUSH_IMAGES" = "true" ; then
+      docker push "${REGISTRY}${ORGANIZATION}/$IMAGE:latest"
+    fi
+  fi
+
+  docker image tag k8sgerrit/$IMAGE:$TAG ${REGISTRY}${ORGANIZATION}/$IMAGE:$TAG
+  if test "$PUSH_IMAGES" = "true" ; then
+    docker push "${REGISTRY}${ORGANIZATION}/$IMAGE:$TAG"
+  fi
+}
+
+if test $# -eq 0 ; then
+  for IMAGE in $IMAGES; do
+    publish_image $IMAGE
+  done
+else
+  while test $# -gt 0 ; do
+    publish_image $1
+    shift
+  done
+fi
diff --git a/charts/k8s-gerrit/setup.cfg b/charts/k8s-gerrit/setup.cfg
new file mode 100644
index 0000000..0c90095
--- /dev/null
+++ b/charts/k8s-gerrit/setup.cfg
@@ -0,0 +1,10 @@
+[tool:pytest]
+norecursedirs = tests/helpers
+
+markers =
+  docker: Tests that require to run and interact with a docker container
+  incremental: Test classes containing tests that need to run incrementally
+  integration: Integration tests
+  kubernetes: Tests that require a Kubernetes cluster
+  slow: Tests that run slower than the average test
+  structure: Structure tests
diff --git a/charts/k8s-gerrit/supplements/gerrit-master.minikube.values.yaml b/charts/k8s-gerrit/supplements/gerrit-master.minikube.values.yaml
new file mode 100644
index 0000000..cb4e7f2
--- /dev/null
+++ b/charts/k8s-gerrit/supplements/gerrit-master.minikube.values.yaml
@@ -0,0 +1,85 @@
+storageClasses:
+  default:
+    name: standard
+  shared:
+    name: shared-storage
+
+gitGC:
+  schedule: "*/15 * * * *"
+
+  resources:
+    requests:
+      cpu: 50m
+      memory: 100Mi
+    limits:
+      cpu: 50m
+      memory: 100Mi
+
+  logging:
+    persistence:
+      enabled: false
+
+gerrit:
+
+  resources:
+    requests:
+      cpu: 200m
+      memory: 400Mi
+    limits:
+      cpu: 500m
+      memory: 400Mi
+
+  persistence:
+    enabled: false
+
+  livenessProbe:
+    initialDelaySeconds: 90
+    periodSeconds: 5
+
+  ingress:
+    host: primary.gerrit
+
+  config:
+    gerrit: |-
+      [gerrit]
+        basePath = git
+        serverId = gerrit-1
+        canonicalWebUrl = http://primary.gerrit
+      [index]
+        type = LUCENE
+      [auth]
+        type = DEVELOPMENT_BECOME_ANY_ACCOUNT
+      [httpd]
+        listenUrl = proxy-http://*:8080/
+      [sshd]
+        listenAddress = off
+      [transfer]
+        timeout = 120 s
+      [user]
+        name = Gerrit Code Review
+        email = gerrit@example.com
+        anonymousCoward = Unnamed User
+      [cache]
+        directory = cache
+      [container]
+        user = gerrit
+        javaHome = /usr/lib/jvm/java-11-openjdk-amd64
+        javaOptions = -Djavax.net.ssl.trustStore=/var/gerrit/etc/keystore
+        javaOptions = -Xms300m
+        javaOptions = -Xmx300m
+
+    secure: |-
+      [remote "replica"]
+        username = git
+        password = secret
+
+    replication: |-
+      [gerrit]
+        autoReload = false
+        replicateOnStartup = true
+        defaultForceUpdate = true
+
+      [remote "replica"]
+        url = http://gerrit-replica-git-backend-service/git/${name}.git
+        replicationDelay = 0
+        timeout = 30
diff --git a/charts/k8s-gerrit/supplements/gerrit-slave.minikube.values.yaml b/charts/k8s-gerrit/supplements/gerrit-slave.minikube.values.yaml
new file mode 100644
index 0000000..e244c3c
--- /dev/null
+++ b/charts/k8s-gerrit/supplements/gerrit-slave.minikube.values.yaml
@@ -0,0 +1,88 @@
+storageClasses:
+  default:
+    name: standard
+  shared:
+    name: shared-storage
+
+gitBackend:
+  resources:
+    requests:
+      cpu: 50m
+      memory: 50Mi
+    limits:
+      cpu: 50m
+      memory: 100Mi
+
+  logging:
+    persistence:
+      enabled: false
+
+  service:
+    type: NodePort
+
+  ingress:
+    enabled: true
+    host: backend.gerrit
+
+gitGC:
+  schedule: "*/15 * * * *"
+
+  resources:
+    requests:
+      cpu: 50m
+      memory: 100Mi
+    limits:
+      cpu: 50m
+      memory: 100Mi
+
+  logging:
+    persistence:
+      enabled: false
+
+gerritReplica:
+  initializeTestSite:
+    enabled: true
+
+  resources:
+    requests:
+      cpu: 200m
+      memory: 400Mi
+    limits:
+      cpu: 500m
+      memory: 400Mi
+
+  persistence:
+    enabled: false
+
+  ingress:
+    host: replica.gerrit
+
+  config:
+    gerrit: |-
+      [gerrit]
+        basePath = git
+        serverId = gerrit-replica-1
+        canonicalWebUrl = http://replica.gerrit
+      [index]
+        type = LUCENE
+      [auth]
+        type = DEVELOPMENT_BECOME_ANY_ACCOUNT
+      [httpd]
+        listenUrl = proxy-http://*:8080/
+      [sshd]
+        listenAddress = off
+      [transfer]
+        timeout = 120 s
+      [user]
+        name = Gerrit Code Review
+        email = gerrit@example.com
+        anonymousCoward = Unnamed User
+      [cache]
+        directory = cache
+      [container]
+        user = gerrit
+        replica = true
+        javaHome = /usr/lib/jvm/java-11-openjdk-amd64
+        javaOptions = -Djavax.net.ssl.trustStore=/var/gerrit/etc/keystore
+        javaOptions = -Xms300m
+        javaOptions = -Xmx300m
diff --git a/charts/k8s-gerrit/supplements/nfs.minikube.values.yaml b/charts/k8s-gerrit/supplements/nfs.minikube.values.yaml
new file mode 100644
index 0000000..dee0761
--- /dev/null
+++ b/charts/k8s-gerrit/supplements/nfs.minikube.values.yaml
@@ -0,0 +1,20 @@
+replicaCount: 1
+
+storageClass:
+  create: true
+  defaultClass: false
+  # The name of the StorageClass has to be the same as the one defined in the
+  # gerrit chart for `storageClasses.shared.name`
+  name: shared-storage
+  parameters:
+    # Required!
+    mountOptions: vers=4.1
+  reclaimPolicy: Delete
+
+resources:
+  requests:
+    cpu: 100m
+    memory: 256Mi
+  limits:
+    cpu: 100m
+    memory: 256Mi
diff --git a/charts/k8s-gerrit/supplements/test-cluster/deploy.sh b/charts/k8s-gerrit/supplements/test-cluster/deploy.sh
new file mode 100755
index 0000000..d157485
--- /dev/null
+++ b/charts/k8s-gerrit/supplements/test-cluster/deploy.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+SCRIPTPATH=`dirname $(readlink -f $0)`
+
+if test -n "$(grep '#TODO' $SCRIPTPATH/**/*.yaml)"; then
+    echo "Incomplete configuration. Replace '#TODO' comments with valid configuration."
+    exit 1
+fi
+
+kubectl apply -f nfs/resources
+helm upgrade nfs-subdir-external-provisioner \
+    nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \
+    --values nfs/nfs-provisioner.values.yaml \
+    --namespace nfs \
+    --install
+
+kubectl apply -f ldap
+kubectl apply -f ingress
+istioctl install -f "$SCRIPTPATH/../../istio/gerrit.profile.yaml"
diff --git a/charts/k8s-gerrit/supplements/test-cluster/ingress/nginx-ingress-controller.yaml b/charts/k8s-gerrit/supplements/test-cluster/ingress/nginx-ingress-controller.yaml
new file mode 100644
index 0000000..afda778
--- /dev/null
+++ b/charts/k8s-gerrit/supplements/test-cluster/ingress/nginx-ingress-controller.yaml
@@ -0,0 +1,627 @@
+#https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.2.0/deploy/static/provider/cloud/deploy.yaml
+apiVersion: v1
+kind: Namespace
+metadata:
+  labels:
+    app.kubernetes.io/instance: ingress-nginx
+    app.kubernetes.io/name: ingress-nginx
+  name: ingress-nginx
+---
+apiVersion: v1
+automountServiceAccountToken: true
+kind: ServiceAccount
+metadata:
+  labels:
+    app.kubernetes.io/component: controller
+    app.kubernetes.io/instance: ingress-nginx
+    app.kubernetes.io/name: ingress-nginx
+    app.kubernetes.io/part-of: ingress-nginx
+    app.kubernetes.io/version: 1.2.0
+  name: ingress-nginx
+  namespace: ingress-nginx
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  labels:
+    app.kubernetes.io/component: admission-webhook
+    app.kubernetes.io/instance: ingress-nginx
+    app.kubernetes.io/name: ingress-nginx
+    app.kubernetes.io/part-of: ingress-nginx
+    app.kubernetes.io/version: 1.2.0
+  name: ingress-nginx-admission
+  namespace: ingress-nginx
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  labels:
+    app.kubernetes.io/component: controller
+    app.kubernetes.io/instance: ingress-nginx
+    app.kubernetes.io/name: ingress-nginx
+    app.kubernetes.io/part-of: ingress-nginx
+    app.kubernetes.io/version: 1.2.0
+  name: ingress-nginx
+  namespace: ingress-nginx
+rules:
+- apiGroups:
+  - ""
+  resources:
+  - namespaces
+  verbs:
+  - get
+- apiGroups:
+  - ""
+  resources:
+  - configmaps
+  - pods
+  - secrets
+  - endpoints
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - ""
+  resources:
+  - services
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - networking.k8s.io
+  resources:
+  - ingresses
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - networking.k8s.io
+  resources:
+  - ingresses/status
+  verbs:
+  - update
+- apiGroups:
+  - networking.k8s.io
+  resources:
+  - ingressclasses
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - ""
+  resourceNames:
+  - ingress-controller-leader
+  resources:
+  - configmaps
+  verbs:
+  - get
+  - update
+- apiGroups:
+  - ""
+  resources:
+  - configmaps
+  verbs:
+  - create
+- apiGroups:
+  - ""
+  resources:
+  - events
+  verbs:
+  - create
+  - patch
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  labels:
+    app.kubernetes.io/component: admission-webhook
+    app.kubernetes.io/instance: ingress-nginx
+    app.kubernetes.io/name: ingress-nginx
+    app.kubernetes.io/part-of: ingress-nginx
+    app.kubernetes.io/version: 1.2.0
+  name: ingress-nginx-admission
+  namespace: ingress-nginx
+rules:
+- apiGroups:
+  - ""
+  resources:
+  - secrets
+  verbs:
+  - get
+  - create
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  labels:
+    app.kubernetes.io/instance: ingress-nginx
+    app.kubernetes.io/name: ingress-nginx
+    app.kubernetes.io/part-of: ingress-nginx
+    app.kubernetes.io/version: 1.2.0
+  name: ingress-nginx
+rules:
+- apiGroups:
+  - ""
+  resources:
+  - configmaps
+  - endpoints
+  - nodes
+  - pods
+  - secrets
+  - namespaces
+  verbs:
+  - list
+  - watch
+- apiGroups:
+  - ""
+  resources:
+  - nodes
+  verbs:
+  - get
+- apiGroups:
+  - ""
+  resources:
+  - services
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - networking.k8s.io
+  resources:
+  - ingresses
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - ""
+  resources:
+  - events
+  verbs:
+  - create
+  - patch
+- apiGroups:
+  - networking.k8s.io
+  resources:
+  - ingresses/status
+  verbs:
+  - update
+- apiGroups:
+  - networking.k8s.io
+  resources:
+  - ingressclasses
+  verbs:
+  - get
+  - list
+  - watch
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  labels:
+    app.kubernetes.io/component: admission-webhook
+    app.kubernetes.io/instance: ingress-nginx
+    app.kubernetes.io/name: ingress-nginx
+    app.kubernetes.io/part-of: ingress-nginx
+    app.kubernetes.io/version: 1.2.0
+  name: ingress-nginx-admission
+rules:
+- apiGroups:
+  - admissionregistration.k8s.io
+  resources:
+  - validatingwebhookconfigurations
+  verbs:
+  - get
+  - update
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  labels:
+    app.kubernetes.io/component: controller
+    app.kubernetes.io/instance: ingress-nginx
+    app.kubernetes.io/name: ingress-nginx
+    app.kubernetes.io/part-of: ingress-nginx
+    app.kubernetes.io/version: 1.2.0
+  name: ingress-nginx
+  namespace: ingress-nginx
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: ingress-nginx
+subjects:
+- kind: ServiceAccount
+  name: ingress-nginx
+  namespace: ingress-nginx
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  labels:
+    app.kubernetes.io/component: admission-webhook
+    app.kubernetes.io/instance: ingress-nginx
+    app.kubernetes.io/name: ingress-nginx
+    app.kubernetes.io/part-of: ingress-nginx
+    app.kubernetes.io/version: 1.2.0
+  name: ingress-nginx-admission
+  namespace: ingress-nginx
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: ingress-nginx-admission
+subjects:
+- kind: ServiceAccount
+  name: ingress-nginx-admission
+  namespace: ingress-nginx
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  labels:
+    app.kubernetes.io/instance: ingress-nginx
+    app.kubernetes.io/name: ingress-nginx
+    app.kubernetes.io/part-of: ingress-nginx
+    app.kubernetes.io/version: 1.2.0
+  name: ingress-nginx
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: ingress-nginx
+subjects:
+- kind: ServiceAccount
+  name: ingress-nginx
+  namespace: ingress-nginx
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  labels:
+    app.kubernetes.io/component: admission-webhook
+    app.kubernetes.io/instance: ingress-nginx
+    app.kubernetes.io/name: ingress-nginx
+    app.kubernetes.io/part-of: ingress-nginx
+    app.kubernetes.io/version: 1.2.0
+  name: ingress-nginx-admission
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: ingress-nginx-admission
+subjects:
+- kind: ServiceAccount
+  name: ingress-nginx-admission
+  namespace: ingress-nginx
+---
+apiVersion: v1
+data:
+  allow-snippet-annotations: "true"
+kind: ConfigMap
+metadata:
+  labels:
+    app.kubernetes.io/component: controller
+    app.kubernetes.io/instance: ingress-nginx
+    app.kubernetes.io/name: ingress-nginx
+    app.kubernetes.io/part-of: ingress-nginx
+    app.kubernetes.io/version: 1.2.0
+  name: ingress-nginx-controller
+  namespace: ingress-nginx
+---
+apiVersion: v1
+kind: Service
+metadata:
+  labels:
+    app.kubernetes.io/component: controller
+    app.kubernetes.io/instance: ingress-nginx
+    app.kubernetes.io/name: ingress-nginx
+    app.kubernetes.io/part-of: ingress-nginx
+    app.kubernetes.io/version: 1.2.0
+  annotations:
+    # NOTE: This only works when using Gardener to manage the cluster
+    cert.gardener.cloud/commonName: #TODO: wildcard ingress URL, e.g. "*.example.com"
+    cert.gardener.cloud/purpose: managed
+    cert.gardener.cloud/secretname: tls-secret
+    dns.gardener.cloud/class: garden
+    dns.gardener.cloud/dnsnames: #TODO: wildcard ingress URL, e.g. "*.example.com"
+    dns.gardener.cloud/ttl: "600"
+  name: ingress-nginx-controller
+  namespace: ingress-nginx
+spec:
+  externalTrafficPolicy: Local
+  ports:
+  - appProtocol: http
+    name: http
+    port: 80
+    protocol: TCP
+    targetPort: http
+  - appProtocol: https
+    name: https
+    port: 443
+    protocol: TCP
+    targetPort: https
+  selector:
+    app.kubernetes.io/component: controller
+    app.kubernetes.io/instance: ingress-nginx
+    app.kubernetes.io/name: ingress-nginx
+  type: LoadBalancer
+---
+apiVersion: v1
+kind: Service
+metadata:
+  labels:
+    app.kubernetes.io/component: controller
+    app.kubernetes.io/instance: ingress-nginx
+    app.kubernetes.io/name: ingress-nginx
+    app.kubernetes.io/part-of: ingress-nginx
+    app.kubernetes.io/version: 1.2.0
+  name: ingress-nginx-controller-admission
+  namespace: ingress-nginx
+spec:
+  ports:
+  - appProtocol: https
+    name: https-webhook
+    port: 443
+    targetPort: webhook
+  selector:
+    app.kubernetes.io/component: controller
+    app.kubernetes.io/instance: ingress-nginx
+    app.kubernetes.io/name: ingress-nginx
+  type: ClusterIP
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  labels:
+    app.kubernetes.io/component: controller
+    app.kubernetes.io/instance: ingress-nginx
+    app.kubernetes.io/name: ingress-nginx
+    app.kubernetes.io/part-of: ingress-nginx
+    app.kubernetes.io/version: 1.2.0
+  name: ingress-nginx-controller
+  namespace: ingress-nginx
+spec:
+  minReadySeconds: 0
+  revisionHistoryLimit: 10
+  selector:
+    matchLabels:
+      app.kubernetes.io/component: controller
+      app.kubernetes.io/instance: ingress-nginx
+      app.kubernetes.io/name: ingress-nginx
+  template:
+    metadata:
+      labels:
+        app.kubernetes.io/component: controller
+        app.kubernetes.io/instance: ingress-nginx
+        app.kubernetes.io/name: ingress-nginx
+    spec:
+      containers:
+      - args:
+        - /nginx-ingress-controller
+        - --publish-service=$(POD_NAMESPACE)/ingress-nginx-controller
+        - --election-id=ingress-controller-leader
+        - --controller-class=k8s.io/ingress-nginx
+        - --ingress-class=nginx
+        - --configmap=$(POD_NAMESPACE)/ingress-nginx-controller
+        - --validating-webhook=:8443
+        - --validating-webhook-certificate=/usr/local/certificates/cert
+        - --validating-webhook-key=/usr/local/certificates/key
+        - --default-ssl-certificate=ingress-nginx/tls-secret
+        env:
+        - name: POD_NAME
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.name
+        - name: POD_NAMESPACE
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.namespace
+        - name: LD_PRELOAD
+          value: /usr/local/lib/libmimalloc.so
+        image: k8s.gcr.io/ingress-nginx/controller:v1.2.0@sha256:d8196e3bc1e72547c5dec66d6556c0ff92a23f6d0919b206be170bc90d5f9185
+        imagePullPolicy: IfNotPresent
+        lifecycle:
+          preStop:
+            exec:
+              command:
+              - /wait-shutdown
+        livenessProbe:
+          failureThreshold: 5
+          httpGet:
+            path: /healthz
+            port: 10254
+            scheme: HTTP
+          initialDelaySeconds: 10
+          periodSeconds: 10
+          successThreshold: 1
+          timeoutSeconds: 1
+        name: controller
+        ports:
+        - containerPort: 80
+          name: http
+          protocol: TCP
+        - containerPort: 443
+          name: https
+          protocol: TCP
+        - containerPort: 8443
+          name: webhook
+          protocol: TCP
+        readinessProbe:
+          failureThreshold: 3
+          httpGet:
+            path: /healthz
+            port: 10254
+            scheme: HTTP
+          initialDelaySeconds: 10
+          periodSeconds: 10
+          successThreshold: 1
+          timeoutSeconds: 1
+        resources:
+          requests:
+            cpu: 100m
+            memory: 90Mi
+        securityContext:
+          allowPrivilegeEscalation: true
+          capabilities:
+            add:
+            - NET_BIND_SERVICE
+            drop:
+            - ALL
+          runAsUser: 101
+        volumeMounts:
+        - mountPath: /usr/local/certificates/
+          name: webhook-cert
+          readOnly: true
+      dnsPolicy: ClusterFirst
+      nodeSelector:
+        kubernetes.io/os: linux
+      serviceAccountName: ingress-nginx
+      terminationGracePeriodSeconds: 300
+      volumes:
+      - name: webhook-cert
+        secret:
+          secretName: ingress-nginx-admission
+---
+apiVersion: batch/v1
+kind: Job
+metadata:
+  labels:
+    app.kubernetes.io/component: admission-webhook
+    app.kubernetes.io/instance: ingress-nginx
+    app.kubernetes.io/name: ingress-nginx
+    app.kubernetes.io/part-of: ingress-nginx
+    app.kubernetes.io/version: 1.2.0
+  name: ingress-nginx-admission-create
+  namespace: ingress-nginx
+spec:
+  template:
+    metadata:
+      labels:
+        app.kubernetes.io/component: admission-webhook
+        app.kubernetes.io/instance: ingress-nginx
+        app.kubernetes.io/name: ingress-nginx
+        app.kubernetes.io/part-of: ingress-nginx
+        app.kubernetes.io/version: 1.2.0
+      name: ingress-nginx-admission-create
+    spec:
+      containers:
+      - args:
+        - create
+        - --host=ingress-nginx-controller-admission,ingress-nginx-controller-admission.$(POD_NAMESPACE).svc
+        - --namespace=$(POD_NAMESPACE)
+        - --secret-name=ingress-nginx-admission
+        env:
+        - name: POD_NAMESPACE
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.namespace
+        image: k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.1.1@sha256:64d8c73dca984af206adf9d6d7e46aa550362b1d7a01f3a0a91b20cc67868660
+        imagePullPolicy: IfNotPresent
+        name: create
+        securityContext:
+          allowPrivilegeEscalation: false
+      nodeSelector:
+        kubernetes.io/os: linux
+      restartPolicy: OnFailure
+      securityContext:
+        fsGroup: 2000
+        runAsNonRoot: true
+        runAsUser: 2000
+      serviceAccountName: ingress-nginx-admission
+---
+apiVersion: batch/v1
+kind: Job
+metadata:
+  labels:
+    app.kubernetes.io/component: admission-webhook
+    app.kubernetes.io/instance: ingress-nginx
+    app.kubernetes.io/name: ingress-nginx
+    app.kubernetes.io/part-of: ingress-nginx
+    app.kubernetes.io/version: 1.2.0
+  name: ingress-nginx-admission-patch
+  namespace: ingress-nginx
+spec:
+  template:
+    metadata:
+      labels:
+        app.kubernetes.io/component: admission-webhook
+        app.kubernetes.io/instance: ingress-nginx
+        app.kubernetes.io/name: ingress-nginx
+        app.kubernetes.io/part-of: ingress-nginx
+        app.kubernetes.io/version: 1.2.0
+      name: ingress-nginx-admission-patch
+    spec:
+      containers:
+      - args:
+        - patch
+        - --webhook-name=ingress-nginx-admission
+        - --namespace=$(POD_NAMESPACE)
+        - --patch-mutating=false
+        - --secret-name=ingress-nginx-admission
+        - --patch-failure-policy=Fail
+        env:
+        - name: POD_NAMESPACE
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.namespace
+        image: k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.1.1@sha256:64d8c73dca984af206adf9d6d7e46aa550362b1d7a01f3a0a91b20cc67868660
+        imagePullPolicy: IfNotPresent
+        name: patch
+        securityContext:
+          allowPrivilegeEscalation: false
+      nodeSelector:
+        kubernetes.io/os: linux
+      restartPolicy: OnFailure
+      securityContext:
+        fsGroup: 2000
+        runAsNonRoot: true
+        runAsUser: 2000
+      serviceAccountName: ingress-nginx-admission
+---
+apiVersion: networking.k8s.io/v1
+kind: IngressClass
+metadata:
+  labels:
+    app.kubernetes.io/component: controller
+    app.kubernetes.io/instance: ingress-nginx
+    app.kubernetes.io/name: ingress-nginx
+    app.kubernetes.io/part-of: ingress-nginx
+    app.kubernetes.io/version: 1.2.0
+  name: nginx
+spec:
+  controller: k8s.io/ingress-nginx
+---
+apiVersion: admissionregistration.k8s.io/v1
+kind: ValidatingWebhookConfiguration
+metadata:
+  labels:
+    app.kubernetes.io/component: admission-webhook
+    app.kubernetes.io/instance: ingress-nginx
+    app.kubernetes.io/name: ingress-nginx
+    app.kubernetes.io/part-of: ingress-nginx
+    app.kubernetes.io/version: 1.2.0
+  name: ingress-nginx-admission
+webhooks:
+- admissionReviewVersions:
+  - v1
+  clientConfig:
+    service:
+      name: ingress-nginx-controller-admission
+      namespace: ingress-nginx
+      path: /networking/v1/ingresses
+  failurePolicy: Fail
+  matchPolicy: Equivalent
+  name: validate.nginx.ingress.kubernetes.io
+  rules:
+  - apiGroups:
+    - networking.k8s.io
+    apiVersions:
+    - v1
+    operations:
+    - CREATE
+    - UPDATE
+    resources:
+    - ingresses
+  sideEffects: None
diff --git a/charts/k8s-gerrit/supplements/test-cluster/ldap/openldap.yaml b/charts/k8s-gerrit/supplements/test-cluster/ldap/openldap.yaml
new file mode 100644
index 0000000..e6e42cc
--- /dev/null
+++ b/charts/k8s-gerrit/supplements/test-cluster/ldap/openldap.yaml
@@ -0,0 +1,85 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: openldap
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: openldap-admin
+  namespace: openldap
+  labels:
+    app: gerrit
+data:
+  adminpassword: #TODO
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: openldap-users
+  namespace: openldap
+  labels:
+    app: gerrit
+data:
+  users: gerrit-admin,gerrit-user
+  passwords: #TODO
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: openldap
+  namespace: openldap
+  labels:
+    app.kubernetes.io/name: openldap
+spec:
+  selector:
+    matchLabels:
+      app.kubernetes.io/name: openldap
+  replicas: 1
+  template:
+    metadata:
+      labels:
+        app.kubernetes.io/name: openldap
+    spec:
+      containers:
+      - name: openldap
+        image: docker.io/bitnami/openldap:latest
+        imagePullPolicy: "IfNotPresent"
+        env:
+          - name: LDAP_ADMIN_USERNAME
+            value: "admin"
+          - name: LDAP_ADMIN_PASSWORD
+            valueFrom:
+              secretKeyRef:
+                key: adminpassword
+                name: openldap-admin
+          - name: LDAP_USERS
+            valueFrom:
+              secretKeyRef:
+                key: users
+                name: openldap-users
+          - name: LDAP_PASSWORDS
+            valueFrom:
+              secretKeyRef:
+                key: passwords
+                name: openldap-users
+        ports:
+          - name: tcp-ldap
+            containerPort: 1389
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: openldap
+  namespace: openldap
+  labels:
+    app.kubernetes.io/name: openldap
+spec:
+  type: ClusterIP
+  ports:
+    - name: tcp-ldap
+      port: 1389
+      targetPort: tcp-ldap
+  selector:
+    app.kubernetes.io/name: openldap
+
diff --git a/charts/k8s-gerrit/supplements/test-cluster/nfs/nfs-provisioner.values.yaml b/charts/k8s-gerrit/supplements/test-cluster/nfs/nfs-provisioner.values.yaml
new file mode 100644
index 0000000..54ddd36
--- /dev/null
+++ b/charts/k8s-gerrit/supplements/test-cluster/nfs/nfs-provisioner.values.yaml
@@ -0,0 +1,8 @@
+nfs:
+  server: #TODO
+  path: #TODO
+
+storageClass:
+  reclaimPolicy: Delete
+  archiveOnDelete: false
+  onDelete: delete
diff --git a/charts/k8s-gerrit/supplements/test-cluster/nfs/resources/nfs.namespace.yaml b/charts/k8s-gerrit/supplements/test-cluster/nfs/resources/nfs.namespace.yaml
new file mode 100644
index 0000000..6545cb0
--- /dev/null
+++ b/charts/k8s-gerrit/supplements/test-cluster/nfs/resources/nfs.namespace.yaml
@@ -0,0 +1,4 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: nfs
diff --git a/charts/k8s-gerrit/tests/conftest.py b/charts/k8s-gerrit/tests/conftest.py
new file mode 100644
index 0000000..eefb8f9
--- /dev/null
+++ b/charts/k8s-gerrit/tests/conftest.py
@@ -0,0 +1,337 @@
+# pylint: disable=W0613, W0212
+
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import argparse
+import getpass
+import os
+import sys
+
+from pathlib import Path
+
+import docker
+import pygit2 as git
+import pytest
+
+sys.path.append(os.path.join(os.path.dirname(__file__), "helpers"))
+
+# pylint: disable=C0103
+pytest_plugins = ["fixtures.credentials", "fixtures.cluster", "fixtures.helm.gerrit"]
+
+# Base images that are not published and thus only tagged with "latest"
+BASE_IMGS = ["base", "gerrit-base"]
+
+
+# pylint: disable=W0622
+class PasswordPromptAction(argparse.Action):
+    def __init__(
+        self,
+        option_strings,
+        dest=None,
+        nargs=0,
+        default=None,
+        required=False,
+        type=None,
+        metavar=None,
+        help=None,
+    ):
+        super().__init__(
+            option_strings=option_strings,
+            dest=dest,
+            nargs=nargs,
+            default=default,
+            required=required,
+            metavar=metavar,
+            type=type,
+            help=help,
+        )
+
+    def __call__(self, parser, args, values, option_string=None):
+        password = getpass.getpass()
+        setattr(args, self.dest, password)
+
+
+def pytest_addoption(parser):
+    parser.addoption(
+        "--registry",
+        action="store",
+        default="",
+        help="Container registry to push (if --push=true) and pull container images"
+        + "from for tests on Kubernetes clusters (default: '')",
+    )
+    parser.addoption(
+        "--registry-user",
+        action="store",
+        default="",
+        help="Username for container registry (default: '')",
+    )
+    parser.addoption(
+        "--registry-pwd",
+        action="store",
+        default="",
+        help="Password for container registry (default: '')",
+    )
+    parser.addoption(
+        "--org",
+        action="store",
+        default="k8sgerrit",
+        help="Docker organization (default: 'k8sgerrit')",
+    )
+    parser.addoption(
+        "--push",
+        action="store_true",
+        help="If set, the docker images will be pushed to the registry configured"
+        + "by --registry (default: false)",
+    )
+    parser.addoption(
+        "--tag",
+        action="store",
+        default=None,
+        help="Tag of cached container images to test. Missing images will be built."
+        + "(default: All container images will be built)",
+    )
+    parser.addoption(
+        "--build-cache",
+        action="store_true",
+        help="If set, the docker cache will be used when building container images.",
+    )
+    parser.addoption(
+        "--kubeconfig",
+        action="store",
+        default=None,
+        help="Kubeconfig to use for cluster connection. If none is given the currently"
+        + "configured context is used.",
+    )
+    parser.addoption(
+        "--rwm-storageclass",
+        action="store",
+        default="shared-storage",
+        help="Name of the storageclass used for ReadWriteMany access."
+        + "(default: shared-storage)",
+    )
+    parser.addoption(
+        "--ingress-url",
+        action="store",
+        default=None,
+        help="URL of the ingress domain used by the cluster.",
+    )
+    parser.addoption(
+        "--gerrit-user",
+        action="store",
+        default="admin",
+        help="Gerrit admin username to be used for smoke tests. (default: admin)",
+    )
+    parser.addoption(
+        "--gerrit-pwd",
+        action=PasswordPromptAction,
+        default="secret",
+        help="Gerrit admin password to be used for smoke tests. (default: secret)",
+    )
+    parser.addoption(
+        "--skip-slow", action="store_true", help="If set, skip slow tests."
+    )
+
+
+def pytest_collection_modifyitems(config, items):
+    if config.getoption("--skip-slow"):
+        skip_slow = pytest.mark.skip(reason="--skip-slow was set.")
+        for item in items:
+            if "slow" in item.keywords:
+                item.add_marker(skip_slow)
+
+
+def pytest_runtest_makereport(item, call):
+    if "incremental" in item.keywords:
+        if call.excinfo is not None:
+            parent = item.parent
+            parent._previousfailed = item
+
+
+def pytest_runtest_setup(item):
+    if "incremental" in item.keywords:
+        previousfailed = getattr(item.parent, "_previousfailed", None)
+        if previousfailed is not None:
+            pytest.xfail(f"previous test failed ({previousfailed.name})")
+
+
+@pytest.fixture(scope="session")
+def tag_of_cached_container(request):
+    return request.config.getoption("--tag")
+
+
+@pytest.fixture(scope="session")
+def docker_client():
+    return docker.from_env()
+
+
+@pytest.fixture(scope="session")
+def repository_root():
+    return Path(git.discover_repository(os.path.realpath(__file__))).parent.absolute()
+
+
+@pytest.fixture(scope="session")
+def container_images(repository_root):
+    image_paths = {}
+    for directory in os.listdir(os.path.join(repository_root, "container-images")):
+        image_paths[directory] = os.path.join(
+            repository_root, "container-images", directory
+        )
+    return image_paths
+
+
+@pytest.fixture(scope="session")
+def docker_registry(request):
+    registry = request.config.getoption("--registry")
+    if registry and not registry[-1] == "/":
+        registry += "/"
+    return registry
+
+
+@pytest.fixture(scope="session")
+def docker_org(request):
+    org = request.config.getoption("--org")
+    if org and not org[-1] == "/":
+        org += "/"
+    return org
+
+
+@pytest.fixture(scope="session")
+def docker_tag(tag_of_cached_container, repository_root):
+    if tag_of_cached_container:
+        return tag_of_cached_container
+    return git.Repository(repository_root).describe(dirty_suffix="-dirty")
+
+
+@pytest.fixture(scope="session")
+def docker_build(
+    request,
+    docker_client,
+    tag_of_cached_container,
+    docker_registry,
+    docker_org,
+    docker_tag,
+):
+    def docker_build(image, name):
+        if name in BASE_IMGS:
+            image_name = f"{name}:latest"
+        else:
+            image_name = f"{docker_registry}{docker_org}{name}:{docker_tag}"
+
+        if tag_of_cached_container:
+            try:
+                return docker_client.images.get(image_name)
+            except docker.errors.ImageNotFound:
+                print(f"Image {image_name} could not be loaded. Building it now.")
+
+        no_cache = not request.config.getoption("--build-cache")
+
+        build = docker_client.images.build(
+            path=image,
+            nocache=no_cache,
+            rm=True,
+            tag=image_name,
+            platform="linux/amd64",
+        )
+        return build[0]
+
+    return docker_build
+
+
+@pytest.fixture(scope="session")
+def docker_login(request, docker_client, docker_registry):
+    username = request.config.getoption("--registry-user")
+    if username:
+        docker_client.login(
+            username=username,
+            password=request.config.getoption("--registry-pwd"),
+            registry=docker_registry,
+        )
+
+
+@pytest.fixture(scope="session")
+def docker_push(
+    request, docker_client, docker_registry, docker_login, docker_org, docker_tag
+):
+    def docker_push(image):
+        docker_repository = f"{docker_registry}{docker_org}{image}"
+        docker_client.images.push(docker_repository, tag=docker_tag)
+
+    return docker_push
+
+
+@pytest.fixture(scope="session")
+def docker_network(request, docker_client):
+    network = docker_client.networks.create(
+        name="k8sgerrit-test-network", scope="local"
+    )
+
+    yield network
+
+    network.remove()
+
+
+@pytest.fixture(scope="session")
+def base_image(container_images, docker_build):
+    return docker_build(container_images["base"], "base")
+
+
+@pytest.fixture(scope="session")
+def gerrit_base_image(container_images, docker_build, base_image):
+    return docker_build(container_images["gerrit-base"], "gerrit-base")
+
+
+@pytest.fixture(scope="session")
+def gitgc_image(request, container_images, docker_build, docker_push, base_image):
+    gitgc_image = docker_build(container_images["git-gc"], "git-gc")
+    if request.config.getoption("--push"):
+        docker_push("git-gc")
+    return gitgc_image
+
+
+@pytest.fixture(scope="session")
+def apache_git_http_backend_image(
+    request, container_images, docker_build, docker_push, base_image
+):
+    apache_git_http_backend_image = docker_build(
+        container_images["apache-git-http-backend"], "apache-git-http-backend"
+    )
+    if request.config.getoption("--push"):
+        docker_push("apache-git-http-backend")
+    return apache_git_http_backend_image
+
+
+@pytest.fixture(scope="session")
+def gerrit_image(
+    request, container_images, docker_build, docker_push, base_image, gerrit_base_image
+):
+    gerrit_image = docker_build(container_images["gerrit"], "gerrit")
+    if request.config.getoption("--push"):
+        docker_push("gerrit")
+    return gerrit_image
+
+
+@pytest.fixture(scope="session")
+def gerrit_init_image(
+    request, container_images, docker_build, docker_push, base_image, gerrit_base_image
+):
+    gerrit_init_image = docker_build(container_images["gerrit-init"], "gerrit-init")
+    if request.config.getoption("--push"):
+        docker_push("gerrit-init")
+    return gerrit_init_image
+
+
+@pytest.fixture(scope="session")
+def required_plugins(request):
+    return ["healthcheck"]
diff --git a/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/conftest.py b/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/conftest.py
new file mode 100644
index 0000000..8cd3443
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/conftest.py
@@ -0,0 +1,92 @@
+# pylint: disable=W0613
+
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import random
+import string
+import time
+
+import pytest
+
+
+class GitBackendContainer:
+    def __init__(self, docker_client, image, port, credentials_dir):
+        self.docker_client = docker_client
+        self.image = image
+        self.port = port
+        self.apache_credentials_dir = credentials_dir
+
+        self.container = None
+
+    def start(self):
+        self.container = self.docker_client.containers.run(
+            image=self.image.id,
+            ports={"80": self.port},
+            volumes={
+                self.apache_credentials_dir: {
+                    "bind": "/var/apache/credentials",
+                    "mode": "ro",
+                }
+            },
+            detach=True,
+            auto_remove=True,
+            platform="linux/amd64",
+        )
+
+    def stop(self):
+        self.container.stop(timeout=1)
+
+
+@pytest.fixture(scope="module")
+def container_run_factory(
+    docker_client, apache_git_http_backend_image, htpasswd, credentials_dir
+):
+    def run_container(port):
+        return GitBackendContainer(
+            docker_client,
+            apache_git_http_backend_image,
+            port,
+            str(credentials_dir),
+        )
+
+    return run_container
+
+
+@pytest.fixture(scope="module")
+def container_run(container_run_factory, free_port):
+    test_setup = container_run_factory(free_port)
+    test_setup.start()
+    time.sleep(3)
+
+    yield test_setup
+
+    test_setup.stop()
+
+
+@pytest.fixture(scope="module")
+def base_url(container_run):
+    return f"http://localhost:{container_run.port}"
+
+
+@pytest.fixture(scope="function")
+def random_repo_name():
+    return "".join(
+        [random.choice(string.ascii_letters + string.digits) for n in range(8)]
+    )
+
+
+@pytest.fixture(scope="function")
+def repo_creation_url(base_url, random_repo_name):
+    return f"{base_url}/a/projects/{random_repo_name}"
diff --git a/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/test_container_build_apache_git_http_backend.py b/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/test_container_build_apache_git_http_backend.py
new file mode 100644
index 0000000..984d6be
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/test_container_build_apache_git_http_backend.py
@@ -0,0 +1,24 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+
+
+@pytest.mark.structure
+def test_build_apache_git_http_backend_image(
+    apache_git_http_backend_image, tag_of_cached_container
+):
+    if tag_of_cached_container:
+        pytest.skip("Cached image used for testing. Build will not be tested.")
+    assert apache_git_http_backend_image.id is not None
diff --git a/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/test_container_integration_apache_git_http_backend.py b/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/test_container_integration_apache_git_http_backend.py
new file mode 100755
index 0000000..0d5ef65
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/test_container_integration_apache_git_http_backend.py
@@ -0,0 +1,96 @@
+# pylint: disable=W0613
+
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from pathlib import Path
+
+import os.path
+
+import pygit2 as git
+import pytest
+import requests
+
+
+@pytest.fixture(scope="function")
+def repo_dir(tmp_path_factory, random_repo_name):
+    return tmp_path_factory.mktemp(random_repo_name)
+
+
+@pytest.fixture(scope="function")
+def mock_repo(repo_dir):
+    repo = git.init_repository(repo_dir, False)
+    file_name = os.path.join(repo_dir, "test.txt")
+    Path(file_name).touch()
+    repo.index.add("test.txt")
+    repo.index.write()
+    # pylint: disable=E1101
+    author = git.Signature("Gerrit Review", "gerrit@review.com")
+    committer = git.Signature("Gerrit Review", "gerrit@review.com")
+    message = "Initial commit"
+    tree = repo.index.write_tree()
+    repo.create_commit("HEAD", author, committer, message, tree, [])
+    return repo
+
+
+@pytest.mark.docker
+@pytest.mark.integration
+def test_apache_git_http_backend_repo_creation(
+    container_run, htpasswd, repo_creation_url
+):
+    request = requests.put(
+        repo_creation_url,
+        auth=requests.auth.HTTPBasicAuth(htpasswd["user"], htpasswd["password"]),
+    )
+    assert request.status_code == 201
+
+
+@pytest.mark.docker
+@pytest.mark.integration
+def test_apache_git_http_backend_repo_creation_fails_without_credentials(
+    container_run, repo_creation_url
+):
+    request = requests.put(repo_creation_url)
+    assert request.status_code == 401
+
+
+@pytest.mark.docker
+@pytest.mark.integration
+def test_apache_git_http_backend_repo_creation_fails_wrong_fs_permissions(
+    container_run, htpasswd, repo_creation_url
+):
+    container_run.container.exec_run("chown -R root:root /var/gerrit/git")
+    request = requests.put(
+        repo_creation_url,
+        auth=requests.auth.HTTPBasicAuth(htpasswd["user"], htpasswd["password"]),
+    )
+    container_run.container.exec_run("chown -R gerrit:users /var/gerrit/git")
+    assert request.status_code == 500
+
+
+@pytest.mark.docker
+@pytest.mark.integration
+def test_apache_git_http_backend_repo_creation_push_repo(
+    container_run, base_url, htpasswd, mock_repo, random_repo_name
+):
+    container_run.container.exec_run(
+        f"su -c 'git init --bare /var/gerrit/git/{random_repo_name}.git' gerrit"
+    )
+    url = f"{base_url}/{random_repo_name}.git"
+    url = url.replace("//", f"//{htpasswd['user']}:{htpasswd['password']}@")
+    origin = mock_repo.remotes.create("origin", url)
+    origin.push(["refs/heads/master:refs/heads/master"])
+
+    remote_refs = origin.ls_remotes()
+    assert str(remote_refs[0]["oid"]) == mock_repo.revparse_single("HEAD").hex
diff --git a/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/test_container_structure_apache_git_http_backend.py b/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/test_container_structure_apache_git_http_backend.py
new file mode 100755
index 0000000..a138ef5
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/apache-git-http-backend/test_container_structure_apache_git_http_backend.py
@@ -0,0 +1,61 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+import utils
+
+
+# pylint: disable=E1101
+@pytest.mark.structure
+def test_apache_git_http_backend_inherits_from_base(apache_git_http_backend_image):
+    assert utils.check_if_ancestor_image_is_inherited(
+        apache_git_http_backend_image, "base:latest"
+    )
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_apache_git_http_backend_contains_apache2(container_run):
+    exit_code, _ = container_run.container.exec_run("which httpd")
+    assert exit_code == 0
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_apache_git_http_backend_http_site_configured(container_run):
+    exit_code, _ = container_run.container.exec_run(
+        "test -f /etc/apache2/conf.d/git-http-backend.conf"
+    )
+    assert exit_code == 0
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_apache_git_http_backend_contains_start_script(container_run):
+    exit_code, _ = container_run.container.exec_run("test -f /var/tools/start")
+    assert exit_code == 0
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_apache_git_http_backend_contains_repo_creation_cgi_script(container_run):
+    exit_code, _ = container_run.container.exec_run("test -f /var/cgi/project_admin.sh")
+    assert exit_code == 0
+
+
+@pytest.mark.structure
+def test_apache_git_http_backend_has_entrypoint(apache_git_http_backend_image):
+    entrypoint = apache_git_http_backend_image.attrs["ContainerConfig"]["Entrypoint"]
+    assert len(entrypoint) == 2
+    assert entrypoint[1] == "/var/tools/start"
diff --git a/charts/k8s-gerrit/tests/container-images/base/test_container_build_base.py b/charts/k8s-gerrit/tests/container-images/base/test_container_build_base.py
new file mode 100644
index 0000000..2a3afa5
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/base/test_container_build_base.py
@@ -0,0 +1,22 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+
+
+@pytest.mark.structure
+def test_build_base(base_image, tag_of_cached_container):
+    if tag_of_cached_container:
+        pytest.skip("Cached image used for testing. Build will not be tested.")
+    assert base_image.id is not None
diff --git a/charts/k8s-gerrit/tests/container-images/base/test_container_structure_base.py b/charts/k8s-gerrit/tests/container-images/base/test_container_structure_base.py
new file mode 100755
index 0000000..528d2b4
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/base/test_container_structure_base.py
@@ -0,0 +1,45 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+
+
+@pytest.fixture(scope="module")
+def container_run(docker_client, container_endless_run_factory, base_image):
+    container_run = container_endless_run_factory(docker_client, base_image)
+    yield container_run
+    container_run.stop(timeout=1)
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_base_contains_git(container_run):
+    exit_code, _ = container_run.exec_run("which git")
+    assert exit_code == 0
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_base_has_non_root_user_gerrit(container_run):
+    exit_code, output = container_run.exec_run("id -u gerrit")
+    assert exit_code == 0
+    uid = int(output.strip().decode("utf-8"))
+    assert uid != 0
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_base_gerrit_no_root_permissions(container_run):
+    exit_code, _ = container_run.exec_run("su -c 'rm -rf /bin' gerrit")
+    assert exit_code > 0
diff --git a/charts/k8s-gerrit/tests/container-images/conftest.py b/charts/k8s-gerrit/tests/container-images/conftest.py
new file mode 100644
index 0000000..8a4b8f2
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/conftest.py
@@ -0,0 +1,105 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os.path
+import socket
+
+import pytest
+
+
+class GerritContainer:
+    def __init__(self, docker_client, docker_network, tmp_dir, image, configs, port):
+        self.docker_client = docker_client
+        self.docker_network = docker_network
+        self.tmp_dir = tmp_dir
+        self.image = image
+        self.configs = configs
+        self.port = port
+
+        self.container = None
+
+    def _create_config_files(self):
+        tmp_config_dir = os.path.join(self.tmp_dir, "configs")
+        if not os.path.isdir(tmp_config_dir):
+            os.mkdir(tmp_config_dir)
+        config_paths = {}
+        for filename, content in self.configs.items():
+            gerrit_config_file = os.path.join(tmp_config_dir, filename)
+            with open(gerrit_config_file, "w", encoding="utf-8") as config_file:
+                config_file.write(content)
+            config_paths[filename] = gerrit_config_file
+        return config_paths
+
+    def _define_volume_mounts(self):
+        volumes = {
+            v: {"bind": f"/var/gerrit/etc/{k}", "mode": "rw"}
+            for (k, v) in self._create_config_files().items()
+        }
+        volumes[os.path.join(self.tmp_dir, "lib")] = {
+            "bind": "/var/gerrit/lib",
+            "mode": "rw",
+        }
+        return volumes
+
+    def start(self):
+        self.container = self.docker_client.containers.run(
+            image=self.image.id,
+            user="gerrit",
+            volumes=self._define_volume_mounts(),
+            ports={8080: str(self.port)},
+            network=self.docker_network.name,
+            detach=True,
+            auto_remove=True,
+            platform="linux/amd64",
+        )
+
+    def stop(self):
+        self.container.stop(timeout=1)
+
+
+@pytest.fixture(scope="session")
+def gerrit_container_factory():
+    def get_gerrit_container(
+        docker_client, docker_network, tmp_dir, image, gerrit_config, port
+    ):
+        return GerritContainer(
+            docker_client, docker_network, tmp_dir, image, gerrit_config, port
+        )
+
+    return get_gerrit_container
+
+
+@pytest.fixture(scope="session")
+def container_endless_run_factory():
+    def get_container(docker_client, image):
+        return docker_client.containers.run(
+            image=image.id,
+            entrypoint="/bin/ash",
+            command=["-c", "tail -f /dev/null"],
+            user="gerrit",
+            detach=True,
+            auto_remove=True,
+            platform="linux/amd64",
+        )
+
+    return get_container
+
+
+@pytest.fixture(scope="session")
+def free_port():
+    skt = socket.socket()
+    skt.bind(("", 0))
+    port = skt.getsockname()[1]
+    skt.close()
+    return port
diff --git a/charts/k8s-gerrit/tests/container-images/gerrit-base/test_container_build_gerrit_base.py b/charts/k8s-gerrit/tests/container-images/gerrit-base/test_container_build_gerrit_base.py
new file mode 100644
index 0000000..93954d8
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/gerrit-base/test_container_build_gerrit_base.py
@@ -0,0 +1,22 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+
+
+@pytest.mark.structure
+def test_build_gerrit_base(gerrit_base_image, tag_of_cached_container):
+    if tag_of_cached_container:
+        pytest.skip("Cached image used for testing. Build will not be tested.")
+    assert gerrit_base_image.id is not None
diff --git a/charts/k8s-gerrit/tests/container-images/gerrit-base/test_container_structure_gerrit_base.py b/charts/k8s-gerrit/tests/container-images/gerrit-base/test_container_structure_gerrit_base.py
new file mode 100755
index 0000000..05161b2
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/gerrit-base/test_container_structure_gerrit_base.py
@@ -0,0 +1,100 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import re
+
+import pytest
+
+import utils
+
+
+JAVA_VER = 11
+
+
+@pytest.fixture(scope="module")
+def container_run(docker_client, container_endless_run_factory, gerrit_base_image):
+    container_run = container_endless_run_factory(docker_client, gerrit_base_image)
+    yield container_run
+    container_run.stop(timeout=1)
+
+
+# pylint: disable=E1101
+@pytest.mark.structure
+def test_gerrit_base_inherits_from_base(gerrit_base_image):
+    assert utils.check_if_ancestor_image_is_inherited(gerrit_base_image, "base:latest")
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_gerrit_base_contains_java(container_run):
+    _, output = container_run.exec_run("java -version")
+    output = output.strip().decode("utf-8")
+    assert re.search(re.compile(f'openjdk version "{JAVA_VER}.[0-9.]+"'), output)
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_gerrit_base_java_path(container_run):
+    exit_code, output = container_run.exec_run(
+        '/bin/ash -c "readlink -f $(which java)"'
+    )
+    output = output.strip().decode("utf-8")
+    assert exit_code == 0
+    assert output == f"/usr/lib/jvm/java-{JAVA_VER}-openjdk/bin/java"
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_gerrit_base_contains_gerrit_war(container_run):
+    exit_code, _ = container_run.exec_run("test -f /var/war/gerrit.war")
+    assert exit_code == 0
+
+    exit_code, _ = container_run.exec_run("test -f /var/gerrit/bin/gerrit.war")
+    assert exit_code == 0
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_gerrit_base_war_contains_gerrit(container_run):
+    exit_code, output = container_run.exec_run("java -jar /var/war/gerrit.war version")
+    assert exit_code == 0
+    output = output.strip().decode("utf-8")
+    assert re.search(re.compile("gerrit version.*"), output)
+
+    exit_code, output = container_run.exec_run(
+        "java -jar /var/gerrit/bin/gerrit.war version"
+    )
+    assert exit_code == 0
+    output = output.strip().decode("utf-8")
+    assert re.search(re.compile("gerrit version.*"), output)
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_gerrit_base_site_permissions(container_run):
+    exit_code, _ = container_run.exec_run("test -O /var/gerrit")
+    assert exit_code == 0
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_gerrit_base_war_dir_permissions(container_run):
+    exit_code, _ = container_run.exec_run("test -O /var/war")
+    assert exit_code == 0
+
+
+@pytest.mark.structure
+def test_gerrit_base_has_entrypoint(gerrit_base_image):
+    entrypoint = gerrit_base_image.attrs["ContainerConfig"]["Entrypoint"]
+    assert "/var/tools/start" in entrypoint
diff --git a/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_build_gerrit_init.py b/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_build_gerrit_init.py
new file mode 100644
index 0000000..dc16d74
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_build_gerrit_init.py
@@ -0,0 +1,22 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+
+
+@pytest.mark.structure
+def test_build_gerrit_init(gerrit_init_image, tag_of_cached_container):
+    if tag_of_cached_container:
+        pytest.skip("Cached image used for testing. Build will not be tested.")
+    assert gerrit_init_image.id is not None
diff --git a/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_integration_gerrit_init.py b/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_integration_gerrit_init.py
new file mode 100644
index 0000000..4dac6e0
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_integration_gerrit_init.py
@@ -0,0 +1,190 @@
+# pylint: disable=E1101
+
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os.path
+import re
+
+from docker.errors import NotFound
+
+import pytest
+import yaml
+
+
+@pytest.fixture(scope="class")
+def container_run_default(request, docker_client, gerrit_init_image, tmp_path_factory):
+    tmp_site_dir = tmp_path_factory.mktemp("gerrit_site")
+    container_run = docker_client.containers.run(
+        image=gerrit_init_image.id,
+        user="gerrit",
+        volumes={tmp_site_dir: {"bind": "/var/gerrit", "mode": "rw"}},
+        detach=True,
+        auto_remove=True,
+        platform="linux/amd64",
+    )
+
+    def stop_container():
+        try:
+            container_run.stop(timeout=1)
+        except NotFound:
+            print("Container already stopped.")
+
+    request.addfinalizer(stop_container)
+
+    return container_run
+
+
+@pytest.fixture(scope="class")
+def init_config_dir(tmp_path_factory):
+    return tmp_path_factory.mktemp("init_config")
+
+
+@pytest.fixture(scope="class")
+def tmp_site_dir(tmp_path_factory):
+    return tmp_path_factory.mktemp("gerrit_site")
+
+
+@pytest.fixture(scope="class")
+def container_run_endless(
+    docker_client, gerrit_init_image, init_config_dir, tmp_site_dir
+):
+    container_run = docker_client.containers.run(
+        image=gerrit_init_image.id,
+        entrypoint="/bin/ash",
+        command=["-c", "tail -f /dev/null"],
+        user="gerrit",
+        volumes={
+            tmp_site_dir: {"bind": "/var/gerrit", "mode": "rw"},
+            init_config_dir: {"bind": "/var/config", "mode": "rw"},
+        },
+        detach=True,
+        auto_remove=True,
+        platform="linux/amd64",
+    )
+
+    yield container_run
+    container_run.stop(timeout=1)
+
+
+@pytest.mark.docker
+@pytest.mark.incremental
+@pytest.mark.integration
+class TestGerritInitEmptySite:
+    @pytest.mark.timeout(60)
+    def test_gerrit_init_gerrit_is_initialized(self, container_run_default):
+        def wait_for_init_success_message():
+            log = container_run_default.logs().decode("utf-8")
+            return log, re.search(r"Initialized /var/gerrit", log)
+
+        while not wait_for_init_success_message():
+            continue
+
+    @pytest.mark.timeout(60)
+    def test_gerrit_init_exits_after_init(self, container_run_default):
+        assert container_run_default.wait()["StatusCode"] == 0
+
+
+@pytest.fixture(
+    scope="function",
+    params=[
+        ["replication", "reviewnotes"],
+        ["replication", "reviewnotes", "hooks"],
+        ["download-commands"],
+        [],
+    ],
+)
+def plugins_to_install(request):
+    return request.param
+
+
+@pytest.mark.docker
+@pytest.mark.incremental
+@pytest.mark.integration
+class TestGerritInitPluginInstallation:
+    def _configure_packaged_plugins(self, file_path, plugins):
+        with open(file_path, "w", encoding="utf-8") as f:
+            yaml.dump(
+                {"plugins": [{"name": p} for p in plugins]}, f, default_flow_style=False
+            )
+
+    def test_gerrit_init_plugins_are_installed(
+        self,
+        container_run_endless,
+        init_config_dir,
+        plugins_to_install,
+        tmp_site_dir,
+        required_plugins,
+    ):
+        self._configure_packaged_plugins(
+            os.path.join(init_config_dir, "init.yaml"), plugins_to_install
+        )
+
+        exit_code, _ = container_run_endless.exec_run(
+            "python3 /var/tools/gerrit-initializer -s /var/gerrit -c /var/config/init.yaml init"
+        )
+        assert exit_code == 0
+
+        plugins_path = os.path.join(tmp_site_dir, "plugins")
+
+        for plugin in plugins_to_install:
+            assert os.path.exists(os.path.join(plugins_path, f"{plugin}.jar"))
+
+        installed_plugins = os.listdir(plugins_path)
+        expected_plugins = plugins_to_install + required_plugins
+        for plugin in installed_plugins:
+            assert os.path.splitext(plugin)[0] in expected_plugins
+
+    def test_required_plugins_are_installed(
+        self, container_run_endless, init_config_dir, tmp_site_dir, required_plugins
+    ):
+        self._configure_packaged_plugins(
+            os.path.join(init_config_dir, "init.yaml"), ["hooks"]
+        )
+
+        exit_code, _ = container_run_endless.exec_run(
+            "python3 /var/tools/gerrit-initializer -s /var/gerrit -c /var/config/init.yaml init"
+        )
+        assert exit_code == 0
+
+        for plugin in required_plugins:
+            assert os.path.exists(
+                os.path.join(tmp_site_dir, "plugins", f"{plugin}.jar")
+            )
+
+    def test_libraries_are_symlinked(
+        self, container_run_endless, init_config_dir, tmp_site_dir
+    ):
+        with open(
+            os.path.join(init_config_dir, "init.yaml"), "w", encoding="utf-8"
+        ) as f:
+            yaml.dump(
+                {"plugins": [{"name": "hooks", "installAsLibrary": True}]},
+                f,
+                default_flow_style=False,
+            )
+
+        exit_code, _ = container_run_endless.exec_run(
+            "python3 /var/tools/gerrit-initializer -s /var/gerrit -c /var/config/init.yaml init"
+        )
+        assert exit_code == 0
+
+        assert os.path.exists(os.path.join(tmp_site_dir, "plugins", "hooks.jar"))
+        assert os.path.islink(os.path.join(tmp_site_dir, "lib", "hooks.jar"))
+
+        exit_code, output = container_run_endless.exec_run(
+            "readlink -f /var/gerrit/lib/hooks.jar"
+        )
+        assert exit_code == 0
+        assert output.decode("utf-8").strip() == "/var/gerrit/plugins/hooks.jar"
diff --git a/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_integration_gerrit_init_reindexing.py b/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_integration_gerrit_init_reindexing.py
new file mode 100644
index 0000000..c8a5b49
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_integration_gerrit_init_reindexing.py
@@ -0,0 +1,160 @@
+# pylint: disable=E1101
+
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+
+import pytest
+
+
+@pytest.fixture(scope="function")
+def temp_site(tmp_path_factory):
+    return tmp_path_factory.mktemp("gerrit-index-test")
+
+
+@pytest.fixture(scope="function")
+def container_run_endless(request, docker_client, gerrit_init_image, temp_site):
+    container_run = docker_client.containers.run(
+        image=gerrit_init_image.id,
+        entrypoint="/bin/ash",
+        command=["-c", "tail -f /dev/null"],
+        volumes={str(temp_site): {"bind": "/var/gerrit", "mode": "rw"}},
+        user="gerrit",
+        detach=True,
+        auto_remove=True,
+        platform="linux/amd64",
+    )
+
+    def stop_container():
+        container_run.stop(timeout=1)
+
+    request.addfinalizer(stop_container)
+
+    return container_run
+
+
+@pytest.mark.incremental
+class TestGerritReindex:
+    def _get_indices(self, container):
+        _, indices = container.exec_run(
+            "git config -f /var/gerrit/index/gerrit_index.config "
+            + "--name-only "
+            + "--get-regexp index"
+        )
+        indices = indices.decode().strip().splitlines()
+        return [index.split(".")[1] for index in indices]
+
+    def test_gerrit_init_skips_reindexing_on_fresh_site(
+        self, temp_site, container_run_endless
+    ):
+        assert not os.path.exists(
+            os.path.join(temp_site, "index", "gerrit_index.config")
+        )
+        exit_code, _ = container_run_endless.exec_run(
+            (
+                "python3 /var/tools/gerrit-initializer "
+                "-s /var/gerrit -c /var/config/gerrit-init.yaml init"
+            )
+        )
+        assert exit_code == 0
+        expected_files = ["gerrit_index.config"] + self._get_indices(
+            container_run_endless
+        )
+        for expected_file in expected_files:
+            assert os.path.exists(os.path.join(temp_site, "index", expected_file))
+
+        timestamp_index_dir = os.path.getctime(os.path.join(temp_site, "index"))
+
+        exit_code, _ = container_run_endless.exec_run(
+            (
+                "python3 /var/tools/gerrit-initializer "
+                "-s /var/gerrit -c /var/config/gerrit-init.yaml reindex"
+            )
+        )
+        assert exit_code == 0
+        assert timestamp_index_dir == os.path.getctime(os.path.join(temp_site, "index"))
+
+    def test_gerrit_init_fixes_missing_index_config(
+        self, container_run_endless, temp_site
+    ):
+        container_run_endless.exec_run(
+            (
+                "python3 /var/tools/gerrit-initializer "
+                "-s /var/gerrit -c /var/config/gerrit-init.yaml init"
+            )
+        )
+        os.remove(os.path.join(temp_site, "index", "gerrit_index.config"))
+
+        exit_code, _ = container_run_endless.exec_run(
+            (
+                "python3 /var/tools/gerrit-initializer "
+                "-s /var/gerrit -c /var/config/gerrit-init.yaml reindex"
+            )
+        )
+        assert exit_code == 0
+
+        exit_code, _ = container_run_endless.exec_run("/var/gerrit/bin/gerrit.sh start")
+        assert exit_code == 0
+
+    def test_gerrit_init_fixes_not_ready_indices(self, container_run_endless):
+        container_run_endless.exec_run(
+            (
+                "python3 /var/tools/gerrit-initializer "
+                "-s /var/gerrit -c /var/config/gerrit-init.yaml init"
+            )
+        )
+
+        indices = self._get_indices(container_run_endless)
+        assert indices
+        container_run_endless.exec_run(
+            f"git config -f /var/gerrit/index/gerrit_index.config {indices[0]} false"
+        )
+
+        exit_code, _ = container_run_endless.exec_run(
+            (
+                "python3 /var/tools/gerrit-initializer "
+                "-s /var/gerrit -c /var/config/gerrit-init.yaml reindex"
+            )
+        )
+        assert exit_code == 0
+
+        exit_code, _ = container_run_endless.exec_run("/var/gerrit/bin/gerrit.sh start")
+        assert exit_code == 0
+
+    def test_gerrit_init_fixes_outdated_indices(self, container_run_endless, temp_site):
+        container_run_endless.exec_run(
+            (
+                "python3 /var/tools/gerrit-initializer "
+                "-s /var/gerrit -c /var/config/gerrit-init.yaml init"
+            )
+        )
+
+        index = self._get_indices(container_run_endless)[0]
+        (name, version) = index.split("_")
+        os.rename(
+            os.path.join(temp_site, "index", index),
+            os.path.join(temp_site, "index", f"{name}_{int(version) - 1:04d}"),
+        )
+
+        exit_code, _ = container_run_endless.exec_run(
+            (
+                "python3 /var/tools/gerrit-initializer "
+                "-s /var/gerrit -c /var/config/gerrit-init.yaml reindex"
+            )
+        )
+        assert exit_code == 0
+
+        exit_code, _ = container_run_endless.exec_run("/var/gerrit/bin/gerrit.sh start")
+        assert exit_code == 0
diff --git a/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_structure_gerrit_init.py b/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_structure_gerrit_init.py
new file mode 100755
index 0000000..5861a5e
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/gerrit-init/test_container_structure_gerrit_init.py
@@ -0,0 +1,74 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+
+import utils
+
+
+@pytest.fixture(scope="module")
+def container_run(docker_client, container_endless_run_factory, gerrit_init_image):
+    container_run = container_endless_run_factory(docker_client, gerrit_init_image)
+    yield container_run
+    container_run.stop(timeout=1)
+
+
+@pytest.fixture(
+    scope="function",
+    params=[
+        "/var/tools/gerrit-initializer/__main__.py",
+        "/var/tools/gerrit-initializer/main.py",
+    ],
+)
+def expected_script(request):
+    return request.param
+
+
+@pytest.fixture(scope="function", params=["python3"])
+def expected_tool(request):
+    return request.param
+
+
+@pytest.fixture(scope="function", params=["pyyaml", "requests"])
+def expected_pip_package(request):
+    return request.param
+
+
+# pylint: disable=E1101
+@pytest.mark.structure
+def test_gerrit_init_inherits_from_gerrit_base(gerrit_init_image):
+    assert utils.check_if_ancestor_image_is_inherited(
+        gerrit_init_image, "gerrit-base:latest"
+    )
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_gerrit_init_contains_expected_scripts(container_run, expected_script):
+    exit_code, _ = container_run.exec_run(f"test -f {expected_script}")
+    assert exit_code == 0
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_gerrit_init_contains_expected_tools(container_run, expected_tool):
+    exit_code, _ = container_run.exec_run(f"which {expected_tool}")
+    assert exit_code == 0
+
+
+@pytest.mark.structure
+def test_gerrit_init_has_entrypoint(gerrit_init_image):
+    entrypoint = gerrit_init_image.attrs["ContainerConfig"]["Entrypoint"]
+    assert len(entrypoint) >= 1
+    assert entrypoint == ["python3", "/var/tools/gerrit-initializer"]
diff --git a/charts/k8s-gerrit/tests/container-images/gerrit/test_container_build_gerrit.py b/charts/k8s-gerrit/tests/container-images/gerrit/test_container_build_gerrit.py
new file mode 100644
index 0000000..a2c3dd5
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/gerrit/test_container_build_gerrit.py
@@ -0,0 +1,22 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+
+
+@pytest.mark.structure
+def test_build_gerrit(gerrit_image, tag_of_cached_container):
+    if tag_of_cached_container:
+        pytest.skip("Cached image used for testing. Build will not be tested.")
+    assert gerrit_image.id is not None
diff --git a/charts/k8s-gerrit/tests/container-images/gerrit/test_container_integration_gerrit.py b/charts/k8s-gerrit/tests/container-images/gerrit/test_container_integration_gerrit.py
new file mode 100644
index 0000000..9376a4a
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/gerrit/test_container_integration_gerrit.py
@@ -0,0 +1,108 @@
+# pylint: disable=W0613, E1101
+
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import re
+import time
+
+import pytest
+import requests
+
+
+@pytest.fixture(scope="module")
+def tmp_dir(tmp_path_factory):
+    return tmp_path_factory.mktemp("gerrit-test")
+
+
+@pytest.fixture(scope="class")
+def container_run(
+    docker_client,
+    docker_network,
+    tmp_dir,
+    gerrit_image,
+    gerrit_container_factory,
+    free_port,
+):
+    configs = {
+        "gerrit.config": """
+      [gerrit]
+        basePath = git
+
+      [httpd]
+        listenUrl = http://*:8080
+
+      [test]
+        success = True
+      """,
+        "secure.config": """
+      [test]
+        success = True
+      """,
+        "replication.config": """
+      [test]
+        success = True
+      """,
+    }
+    test_setup = gerrit_container_factory(
+        docker_client, docker_network, tmp_dir, gerrit_image, configs, free_port
+    )
+    test_setup.start()
+
+    yield test_setup
+
+    test_setup.stop()
+
+
+@pytest.fixture(params=["gerrit.config", "secure.config", "replication.config"])
+def config_file_to_test(request):
+    return request.param
+
+
+@pytest.mark.docker
+@pytest.mark.incremental
+@pytest.mark.integration
+@pytest.mark.slow
+class TestGerritStartScript:
+    @pytest.mark.timeout(60)
+    def test_gerrit_gerrit_starts_up(self, container_run):
+        def wait_for_gerrit_start():
+            log = container_run.container.logs().decode("utf-8")
+            return re.search(r"Gerrit Code Review .+ ready", log)
+
+        while not wait_for_gerrit_start:
+            continue
+
+    def test_gerrit_custom_gerrit_config_available(
+        self, container_run, config_file_to_test
+    ):
+        exit_code, output = container_run.container.exec_run(
+            f"git config --file=/var/gerrit/etc/{config_file_to_test} --get test.success"
+        )
+        output = output.decode("utf-8").strip()
+        assert exit_code == 0
+        assert output == "True"
+
+    @pytest.mark.timeout(60)
+    def test_gerrit_httpd_is_responding(self, container_run):
+        status = None
+        while not status == 200:
+            try:
+                response = requests.get(f"http://localhost:{container_run.port}")
+                status = response.status_code
+            except requests.exceptions.ConnectionError:
+                time.sleep(1)
+
+        assert response.status_code == 200
+        assert re.search(r'content="Gerrit Code Review"', response.text)
diff --git a/charts/k8s-gerrit/tests/container-images/gerrit/test_container_integration_gerrit_replica.py b/charts/k8s-gerrit/tests/container-images/gerrit/test_container_integration_gerrit_replica.py
new file mode 100644
index 0000000..3673eab
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/gerrit/test_container_integration_gerrit_replica.py
@@ -0,0 +1,122 @@
+# pylint: disable=W0613, E1101
+
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import os.path
+import re
+
+import pygit2 as git
+import pytest
+import requests
+
+CONFIG_FILES = ["gerrit.config", "secure.config"]
+
+
+@pytest.fixture(scope="module")
+def tmp_dir(tmp_path_factory):
+    return tmp_path_factory.mktemp("gerrit-replica-test")
+
+
+@pytest.fixture(scope="class")
+def container_run(
+    request,
+    docker_client,
+    docker_network,
+    tmp_dir,
+    gerrit_image,
+    gerrit_container_factory,
+    free_port,
+):
+    configs = {
+        "gerrit.config": """
+      [gerrit]
+        basePath = git
+
+      [httpd]
+        listenUrl = http://*:8080
+
+      [container]
+        replica = true
+
+      [test]
+        success = True
+      """,
+        "secure.config": """
+      [test]
+          success = True
+      """,
+    }
+
+    test_setup = gerrit_container_factory(
+        docker_client, docker_network, tmp_dir, gerrit_image, configs, free_port
+    )
+    test_setup.start()
+
+    request.addfinalizer(test_setup.stop)
+
+    return test_setup
+
+
+@pytest.mark.docker
+@pytest.mark.incremental
+@pytest.mark.integration
+@pytest.mark.slow
+class TestGerritReplica:
+    @pytest.fixture(params=CONFIG_FILES)
+    def config_file_to_test(self, request):
+        return request.param
+
+    @pytest.fixture(params=["All-Users.git", "All-Projects.git"])
+    def expected_repository(self, request):
+        return request.param
+
+    @pytest.mark.timeout(60)
+    def test_gerrit_replica_gerrit_starts_up(self, container_run):
+        def wait_for_gerrit_start():
+            log = container_run.container.logs().decode("utf-8")
+            return re.search(r"Gerrit Code Review .+ ready", log)
+
+        while not wait_for_gerrit_start():
+            continue
+
+    def test_gerrit_replica_custom_gerrit_config_available(
+        self, container_run, config_file_to_test
+    ):
+        exit_code, output = container_run.container.exec_run(
+            f"git config --file=/var/gerrit/etc/{config_file_to_test} --get test.success"
+        )
+        output = output.decode("utf-8").strip()
+        assert exit_code == 0
+        assert output == "True"
+
+    def test_gerrit_replica_repository_exists(self, container_run, expected_repository):
+        exit_code, _ = container_run.container.exec_run(
+            f"test -d /var/gerrit/git/{expected_repository}"
+        )
+        assert exit_code == 0
+
+    def test_gerrit_replica_clone_repo_works(self, container_run, tmp_path_factory):
+        container_run.container.exec_run("git init --bare /var/gerrit/git/test.git")
+        clone_dest = tmp_path_factory.mktemp("gerrit_replica_clone_test")
+        repo = git.clone_repository(
+            f"http://localhost:{container_run.port}/test.git", clone_dest
+        )
+        assert repo.path == os.path.join(clone_dest, ".git/")
+
+    def test_gerrit_replica_webui_not_accessible(self, container_run):
+        response = requests.get(f"http://localhost:{container_run.port}")
+        assert response.status_code == 404
+        assert response.text == "Not Found"
diff --git a/charts/k8s-gerrit/tests/container-images/gerrit/test_container_structure_gerrit.py b/charts/k8s-gerrit/tests/container-images/gerrit/test_container_structure_gerrit.py
new file mode 100755
index 0000000..7ece25e
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/gerrit/test_container_structure_gerrit.py
@@ -0,0 +1,39 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+
+import utils
+
+
+@pytest.fixture(scope="module")
+def container_run(docker_client, container_endless_run_factory, gerrit_image):
+    container_run = container_endless_run_factory(docker_client, gerrit_image)
+    yield container_run
+    container_run.stop(timeout=1)
+
+
+# pylint: disable=E1101
+@pytest.mark.structure
+def test_gerrit_inherits_from_gerrit_base(gerrit_image):
+    assert utils.check_if_ancestor_image_is_inherited(
+        gerrit_image, "gerrit-base:latest"
+    )
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_gerrit_contains_start_script(container_run):
+    exit_code, _ = container_run.exec_run("test -f /var/tools/start")
+    assert exit_code == 0
diff --git a/charts/k8s-gerrit/tests/container-images/git-gc/test_container_build_gitgc.py b/charts/k8s-gerrit/tests/container-images/git-gc/test_container_build_gitgc.py
new file mode 100644
index 0000000..a640d20
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/git-gc/test_container_build_gitgc.py
@@ -0,0 +1,22 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+
+
+@pytest.mark.structure
+def test_build_gitgc(gitgc_image, tag_of_cached_container):
+    if tag_of_cached_container:
+        pytest.skip("Cached image used for testing. Build will not be tested.")
+    assert gitgc_image.id is not None
diff --git a/charts/k8s-gerrit/tests/container-images/git-gc/test_container_structure_gitgc.py b/charts/k8s-gerrit/tests/container-images/git-gc/test_container_structure_gitgc.py
new file mode 100644
index 0000000..9f03644
--- /dev/null
+++ b/charts/k8s-gerrit/tests/container-images/git-gc/test_container_structure_gitgc.py
@@ -0,0 +1,51 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+
+import utils
+
+
+@pytest.fixture(scope="module")
+def container_run(docker_client, container_endless_run_factory, gitgc_image):
+    container_run = container_endless_run_factory(docker_client, gitgc_image)
+    yield container_run
+    container_run.stop(timeout=1)
+
+
+# pylint: disable=E1101
+@pytest.mark.structure
+def test_gitgc_inherits_from_base(gitgc_image):
+    assert utils.check_if_ancestor_image_is_inherited(gitgc_image, "base:latest")
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_gitgc_log_dir_writable_by_gerrit(container_run):
+    exit_code, _ = container_run.exec_run("touch /var/log/git/test.log")
+    assert exit_code == 0
+
+
+@pytest.mark.docker
+@pytest.mark.structure
+def test_gitgc_contains_gc_script(container_run):
+    exit_code, _ = container_run.exec_run("test -f /var/tools/gc.sh")
+    assert exit_code == 0
+
+
+@pytest.mark.structure
+def test_gitgc_has_entrypoint(gitgc_image):
+    entrypoint = gitgc_image.attrs["ContainerConfig"]["Entrypoint"]
+    assert len(entrypoint) == 1
+    assert entrypoint[0] == "/var/tools/gc.sh"
diff --git a/charts/k8s-gerrit/tests/fixtures/__init__.py b/charts/k8s-gerrit/tests/fixtures/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/charts/k8s-gerrit/tests/fixtures/__init__.py
diff --git a/charts/k8s-gerrit/tests/fixtures/cluster.py b/charts/k8s-gerrit/tests/fixtures/cluster.py
new file mode 100644
index 0000000..eb94968
--- /dev/null
+++ b/charts/k8s-gerrit/tests/fixtures/cluster.py
@@ -0,0 +1,144 @@
+# pylint: disable=W0613
+
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import base64
+import json
+import warnings
+
+from kubernetes import client, config
+
+import pytest
+
+from .helm.client import HelmClient
+
+
+class Cluster:
+    def __init__(self, kube_config):
+        self.kube_config = kube_config
+
+        self.image_pull_secrets = []
+        self.namespaces = []
+
+        context = self._load_kube_config()
+        self.helm = HelmClient(self.kube_config, context)
+
+    def _load_kube_config(self):
+        config.load_kube_config(config_file=self.kube_config)
+        _, context = config.list_kube_config_contexts(config_file=self.kube_config)
+        return context["name"]
+
+    def _apply_image_pull_secrets(self, namespace):
+        for ips in self.image_pull_secrets:
+            try:
+                client.CoreV1Api().create_namespaced_secret(namespace, ips)
+            except client.rest.ApiException as exc:
+                if exc.status == 409 and exc.reason == "Conflict":
+                    warnings.warn(
+                        "Kubernetes Cluster not empty. Image pull secret already exists."
+                    )
+                else:
+                    raise exc
+
+    def add_container_registry(self, secret_name, url, user, pwd):
+        data = {
+            "auths": {
+                url: {
+                    "auth": base64.b64encode(str.encode(f"{user}:{pwd}")).decode(
+                        "utf-8"
+                    )
+                }
+            }
+        }
+        metadata = client.V1ObjectMeta(name=secret_name)
+        self.image_pull_secrets.append(
+            client.V1Secret(
+                api_version="v1",
+                kind="Secret",
+                metadata=metadata,
+                type="kubernetes.io/dockerconfigjson",
+                data={
+                    ".dockerconfigjson": base64.b64encode(
+                        json.dumps(data).encode()
+                    ).decode("utf-8")
+                },
+            )
+        )
+
+    def create_namespace(self, name):
+        namespace_metadata = client.V1ObjectMeta(name=name)
+        namespace_body = client.V1Namespace(
+            kind="Namespace", api_version="v1", metadata=namespace_metadata
+        )
+        client.CoreV1Api().create_namespace(body=namespace_body)
+        self.namespaces.append(name)
+        self._apply_image_pull_secrets(name)
+
+    def delete_namespace(self, name):
+        if name not in self.namespaces:
+            return
+
+        client.CoreV1Api().delete_namespace(name, body=client.V1DeleteOptions())
+        self.namespaces.remove(name)
+
+    def cleanup(self):
+        while self.namespaces:
+            self.helm.delete_all(
+                namespace=self.namespaces[0],
+            )
+            self.delete_namespace(self.namespaces[0])
+
+
+@pytest.fixture(scope="session")
+def test_cluster(request):
+    kube_config = request.config.getoption("--kubeconfig")
+
+    test_cluster = Cluster(kube_config)
+    test_cluster.add_container_registry(
+        "image-pull-secret",
+        request.config.getoption("--registry"),
+        request.config.getoption("--registry-user"),
+        request.config.getoption("--registry-pwd"),
+    )
+
+    yield test_cluster
+
+    test_cluster.cleanup()
+
+
+@pytest.fixture(scope="session")
+def ldap_credentials(test_cluster):
+    ldap_secret = client.CoreV1Api().read_namespaced_secret(
+        "openldap-users", namespace="openldap"
+    )
+    users = base64.b64decode(ldap_secret.data["users"]).decode("utf-8").split(",")
+    passwords = (
+        base64.b64decode(ldap_secret.data["passwords"]).decode("utf-8").split(",")
+    )
+    credentials = {}
+    for i, user in enumerate(users):
+        credentials[user] = passwords[i]
+
+    yield credentials
+
+
+@pytest.fixture(scope="session")
+def ldap_admin_credentials(test_cluster):
+    ldap_secret = client.CoreV1Api().read_namespaced_secret(
+        "openldap-admin", namespace="openldap"
+    )
+    password = base64.b64decode(ldap_secret.data["adminpassword"]).decode("utf-8")
+
+    yield ("admin", password)
diff --git a/charts/k8s-gerrit/tests/fixtures/credentials.py b/charts/k8s-gerrit/tests/fixtures/credentials.py
new file mode 100644
index 0000000..de39dc1
--- /dev/null
+++ b/charts/k8s-gerrit/tests/fixtures/credentials.py
@@ -0,0 +1,39 @@
+# pylint: disable=W0613
+
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+
+import pytest
+
+from passlib.apache import HtpasswdFile
+
+import utils
+
+
+@pytest.fixture(scope="session")
+def credentials_dir(tmp_path_factory):
+    return tmp_path_factory.mktemp("creds")
+
+
+@pytest.fixture(scope="session")
+def htpasswd(credentials_dir):
+    basic_auth_creds = {"user": "admin", "password": utils.create_random_string(16)}
+    htpasswd_file = HtpasswdFile(os.path.join(credentials_dir, ".htpasswd"), new=True)
+    htpasswd_file.set_password(basic_auth_creds["user"], basic_auth_creds["password"])
+    htpasswd_file.save()
+    basic_auth_creds["htpasswd_string"] = htpasswd_file.to_string()
+    basic_auth_creds["htpasswd_file"] = credentials_dir
+    yield basic_auth_creds
diff --git a/charts/k8s-gerrit/tests/fixtures/helm/__init__.py b/charts/k8s-gerrit/tests/fixtures/helm/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/charts/k8s-gerrit/tests/fixtures/helm/__init__.py
diff --git a/charts/k8s-gerrit/tests/fixtures/helm/abstract_deployment.py b/charts/k8s-gerrit/tests/fixtures/helm/abstract_deployment.py
new file mode 100644
index 0000000..517cfe2
--- /dev/null
+++ b/charts/k8s-gerrit/tests/fixtures/helm/abstract_deployment.py
@@ -0,0 +1,99 @@
+import abc
+import random
+import re
+import string
+
+from time import time
+
+from kubernetes import client
+
+
+class AbstractDeployment(abc.ABC):
+    def __init__(self, tmp_dir):
+        self.tmp_dir = tmp_dir
+        self.namespace = "".join(
+            [random.choice(string.ascii_letters) for n in range(8)]
+        ).lower()
+        self.values_file = self._set_values_file()
+        self.chart_opts = {}
+
+    @abc.abstractmethod
+    def install(self, wait=True):
+        pass
+
+    @abc.abstractmethod
+    def update(self):
+        pass
+
+    @abc.abstractmethod
+    def uninstall(self):
+        pass
+
+    @abc.abstractmethod
+    def _set_values_file(self):
+        pass
+
+    def set_helm_value(self, combined_key, value):
+        nested_keys = re.split(r"(?<!\\)\.", combined_key)
+        dct_pointer = self.chart_opts
+        for key in nested_keys[:-1]:
+            # pylint: disable=W1401
+            key.replace("\.", ".")
+            dct_pointer = dct_pointer.setdefault(key, {})
+        # pylint: disable=W1401
+        dct_pointer[nested_keys[-1].replace("\.", ".")] = value
+
+    def _wait_for_pod_readiness(self, pod_labels, timeout=180):
+        """Helper function that can be used to wait for all pods with a given set of
+        labels to be ready.
+
+        Arguments:
+        pod_labels {str} -- Label selector string to be used to select pods.
+            (https://kubernetes.io/docs/concepts/overview/working-with-objects/\
+                labels/#label-selectors)
+
+        Keyword Arguments:
+        timeout {int} -- Time in seconds to wait for the pod status to become ready.
+            (default: {180})
+
+        Returns:
+        boolean -- Whether pods were ready in time.
+        """
+
+        def check_pod_readiness():
+            core_v1 = client.CoreV1Api()
+            pod_list = core_v1.list_pod_for_all_namespaces(
+                watch=False, label_selector=pod_labels
+            )
+            for pod in pod_list.items:
+                for condition in pod.status.conditions:
+                    if condition.type != "Ready" and condition.status != "True":
+                        return False
+            return True
+
+        return self._exec_fn_with_timeout(check_pod_readiness, limit=timeout)
+
+    def _exec_fn_with_timeout(self, func, limit=60):
+        """Helper function that executes a given function until it returns True or a
+        given time limit is reached.
+
+        Arguments:
+        func {function} -- Function to execute. The function can return some output
+                        (or None) and as a second return value a boolean indicating,
+                        whether the event the function was waiting for has happened.
+
+        Keyword Arguments:
+        limit {int} -- Maximum time in seconds to wait for a positive response of
+                        the function (default: {60})
+
+        Returns:
+        boolean -- False, if the timeout was reached
+        any -- Last output of fn
+        """
+
+        timeout = time() + limit
+        while time() < timeout:
+            is_finished = func()
+            if is_finished:
+                return True
+        return False
diff --git a/charts/k8s-gerrit/tests/fixtures/helm/client.py b/charts/k8s-gerrit/tests/fixtures/helm/client.py
new file mode 100644
index 0000000..eb3285f
--- /dev/null
+++ b/charts/k8s-gerrit/tests/fixtures/helm/client.py
@@ -0,0 +1,202 @@
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+import subprocess
+
+
+class HelmClient:
+    def __init__(self, kubeconfig, kubecontext):
+        """Wrapper for Helm CLI.
+
+        Arguments:
+            kubeconfig {str} -- Path to kubeconfig-file describing the cluster to
+                                connect to.
+            kubecontext {str} -- Name of the context to use.
+        """
+
+        self.kubeconfig = kubeconfig
+        self.kubecontext = kubecontext
+
+    def _exec_command(self, cmd, fail_on_err=True):
+        base_cmd = [
+            "helm",
+            "--kubeconfig",
+            self.kubeconfig,
+            "--kube-context",
+            self.kubecontext,
+        ]
+        return subprocess.run(
+            base_cmd + cmd,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+            check=fail_on_err,
+            text=True,
+        )
+
+    def install(
+        self,
+        chart,
+        name,
+        values_file=None,
+        set_values=None,
+        namespace=None,
+        fail_on_err=True,
+        wait=True,
+    ):
+        """Installs a chart on the cluster
+
+        Arguments:
+            chart {str} -- Release name or path of a helm chart
+            name {str} -- Name with which the chart will be installed on the cluster
+
+        Keyword Arguments:
+            values_file {str} -- Path to a custom values.yaml file (default: {None})
+            set_values {dict} -- Dictionary containing key-value-pairs that are used
+                                to overwrite values in the values.yaml-file.
+                                (default: {None})
+            namespace {str} -- Namespace to install the release into (default: {default})
+            fail_on_err {bool} -- Whether to fail with an exception if the installation
+                                fails (default: {True})
+            wait {bool} -- Whether to wait for all pods to be ready (default: {True})
+
+        Returns:
+            CompletedProcess -- CompletedProcess-object returned by subprocess
+                                containing details about the result and output of the
+                                executed command.
+        """
+
+        helm_cmd = ["install", name, chart, "--dependency-update"]
+        if values_file:
+            helm_cmd.extend(("-f", values_file))
+        if set_values:
+            opt_list = [f"{k}={v}" for k, v in set_values.items()]
+            helm_cmd.extend(("--set", ",".join(opt_list)))
+        if namespace:
+            helm_cmd.extend(("--namespace", namespace))
+        if wait:
+            helm_cmd.append("--wait")
+        return self._exec_command(helm_cmd, fail_on_err)
+
+    def list(self, namespace=None):
+        """Lists helm charts installed on the cluster.
+
+        Keyword Arguments:
+            namespace {str} -- Kubernetes namespace (default: {None})
+
+        Returns:
+            list -- List of helm chart realeases installed on the cluster.
+        """
+
+        helm_cmd = ["list", "--all", "--output", "json"]
+        if namespace:
+            helm_cmd.extend(("--namespace", namespace))
+        output = self._exec_command(helm_cmd).stdout
+        return json.loads(output)
+
+    def upgrade(
+        self,
+        chart,
+        name,
+        namespace,
+        values_file=None,
+        set_values=None,
+        reuse_values=True,
+        fail_on_err=True,
+    ):
+        """Updates a chart on the cluster
+
+        Arguments:
+            chart {str} -- Release name or path of a helm chart
+            name {str} -- Name with which the chart will be installed on the cluster
+            namespace {str} -- Kubernetes namespace
+
+        Keyword Arguments:
+            values_file {str} -- Path to a custom values.yaml file (default: {None})
+            set_values {dict} -- Dictionary containing key-value-pairs that are used
+                                to overwrite values in the values.yaml-file.
+                                (default: {None})
+            reuse_values {bool} -- Whether to reuse existing not overwritten values
+                                (default: {True})
+            fail_on_err {bool} -- Whether to fail with an exception if the installation
+                                fails (default: {True})
+
+        Returns:
+            CompletedProcess -- CompletedProcess-object returned by subprocess
+                                containing details about the result and output of the
+                                executed command.
+        """
+        helm_cmd = ["upgrade", name, chart, "--namespace", namespace, "--wait"]
+        if values_file:
+            helm_cmd.extend(("-f", values_file))
+        if reuse_values:
+            helm_cmd.append("--reuse-values")
+        if set_values:
+            opt_list = [f"{k}={v}" for k, v in set_values.items()]
+            helm_cmd.extend(("--set", ",".join(opt_list)))
+        return self._exec_command(helm_cmd, fail_on_err)
+
+    def delete(self, name, namespace=None):
+        """Deletes a chart from the cluster
+
+        Arguments:
+            name {str} -- Name of the chart to delete
+
+        Keyword Arguments:
+            namespace {str} -- Kubernetes namespace (default: {None})
+
+        Returns:
+            CompletedProcess -- CompletedProcess-object returned by subprocess
+                                containing details about the result and output of
+                                the executed command.
+        """
+
+        if name not in self.list(namespace):
+            return None
+
+        helm_cmd = ["delete", name]
+        if namespace:
+            helm_cmd.extend(("--namespace", namespace))
+        return self._exec_command(helm_cmd)
+
+    def delete_all(self, namespace=None, exceptions=None):
+        """Deletes all charts on the cluster
+
+        Keyword Arguments:
+            namespace {str} -- Kubernetes namespace (default: {None})
+            exceptions {list} -- List of chart names not to delete (default: {None})
+        """
+
+        charts = self.list(namespace)
+        for chart in charts:
+            if exceptions and chart["name"] in exceptions:
+                continue
+            self.delete(chart["name"], namespace)
+
+    def is_installed(self, namespace, chart):
+        """Checks if a chart is installed in the cluster
+
+        Keyword Arguments:
+            namespace {str} -- Kubernetes namespace
+            chart {str} -- Name of the chart
+
+        Returns:
+            bool -- Whether the chart is installed
+        """
+
+        for installed_chart in self.list(namespace):
+            if installed_chart["name"] == chart:
+                return True
+
+        return False
diff --git a/charts/k8s-gerrit/tests/fixtures/helm/gerrit.py b/charts/k8s-gerrit/tests/fixtures/helm/gerrit.py
new file mode 100644
index 0000000..ec7a7c1
--- /dev/null
+++ b/charts/k8s-gerrit/tests/fixtures/helm/gerrit.py
@@ -0,0 +1,279 @@
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os.path
+
+from copy import deepcopy
+from pathlib import Path
+
+import pytest
+import yaml
+
+import pygit2 as git
+import chromedriver_autoinstaller
+from kubernetes import client
+from selenium import webdriver
+from selenium.webdriver.common.by import By
+
+from .abstract_deployment import AbstractDeployment
+
+
+class TimeOutException(Exception):
+    """Exception to be raised, if some action does not finish in time."""
+
+
+def dict_to_git_config(config_dict):
+    config = ""
+    for section, options in config_dict.items():
+        config += f"[{section}]\n"
+        for key, value in options.items():
+            if isinstance(value, bool):
+                value = "true" if value else "false"
+            elif isinstance(value, list):
+                for opt in value:
+                    config += f"  {key} = {opt}\n"
+                continue
+            config += f"  {key} = {value}\n"
+    return config
+
+
+GERRIT_STARTUP_TIMEOUT = 240
+
+DEFAULT_GERRIT_CONFIG = {
+    "auth": {
+        "type": "LDAP",
+    },
+    "container": {
+        "user": "gerrit",
+        "javaHome": "/usr/lib/jvm/java-11-openjdk",
+        "javaOptions": [
+            "-Djavax.net.ssl.trustStore=/var/gerrit/etc/keystore",
+            "-Xms200m",
+            "-Xmx4g",
+        ],
+    },
+    "gerrit": {
+        "basePath": "git",
+        "canonicalWebUrl": "http://example.com/",
+        "serverId": "gerrit-1",
+    },
+    "httpd": {
+        "listenUrl": "proxy-https://*:8080/",
+        "requestLog": True,
+        "gracefulStopTimeout": "1m",
+    },
+    "index": {"type": "LUCENE", "onlineUpgrade": False},
+    "ldap": {
+        "server": "ldap://openldap.openldap.svc.cluster.local:1389",
+        "accountbase": "dc=example,dc=org",
+        "username": "cn=admin,dc=example,dc=org",
+    },
+    "sshd": {"listenAddress": "off"},
+}
+
+DEFAULT_VALUES = {
+    "gitRepositoryStorage": {"externalPVC": {"use": True, "name": "repo-storage"}},
+    "gitGC": {"logging": {"persistence": {"enabled": False}}},
+    "gerrit": {
+        "etc": {"config": {"gerrit.config": dict_to_git_config(DEFAULT_GERRIT_CONFIG)}}
+    },
+}
+
+
+# pylint: disable=R0902
+class GerritDeployment(AbstractDeployment):
+    def __init__(
+        self,
+        tmp_dir,
+        cluster,
+        storageclass,
+        container_registry,
+        container_org,
+        container_version,
+        ingress_url,
+        ldap_admin_credentials,
+        ldap_credentials,
+    ):
+        super().__init__(tmp_dir)
+        self.cluster = cluster
+        self.storageclass = storageclass
+        self.ldap_credentials = ldap_credentials
+
+        self.chart_name = "gerrit-" + self.namespace
+        self.chart_path = os.path.join(
+            # pylint: disable=E1101
+            Path(git.discover_repository(os.path.realpath(__file__))).parent.absolute(),
+            "helm-charts",
+            "gerrit",
+        )
+
+        self.gerrit_config = deepcopy(DEFAULT_GERRIT_CONFIG)
+        self.chart_opts = deepcopy(DEFAULT_VALUES)
+
+        self._configure_container_images(
+            container_registry, container_org, container_version
+        )
+        self.hostname = f"{self.namespace}.{ingress_url}"
+        self._configure_ingress()
+        self.set_gerrit_config_value(
+            "gerrit", "canonicalWebUrl", f"http://{self.hostname}"
+        )
+        # pylint: disable=W1401
+        self.set_helm_value(
+            "gerrit.etc.secret.secure\.config",
+            dict_to_git_config({"ldap": {"password": ldap_admin_credentials[1]}}),
+        )
+
+    def install(self, wait=True):
+        if self.cluster.helm.is_installed(self.namespace, self.chart_name):
+            self.update()
+            return
+
+        with open(self.values_file, "w", encoding="UTF-8") as f:
+            yaml.dump(self.chart_opts, f)
+
+        self.cluster.create_namespace(self.namespace)
+        self._create_pvc()
+
+        self.cluster.helm.install(
+            self.chart_path,
+            self.chart_name,
+            values_file=self.values_file,
+            fail_on_err=True,
+            namespace=self.namespace,
+            wait=wait,
+        )
+
+    def create_admin_account(self):
+        self.wait_until_ready()
+        chromedriver_autoinstaller.install()
+        options = webdriver.ChromeOptions()
+        options.add_argument("--headless")
+        options.add_argument("--no-sandbox")
+        options.add_argument("--ignore-certificate-errors")
+        options.set_capability("acceptInsecureCerts", True)
+        driver = webdriver.Chrome(
+            options=options,
+        )
+        driver.get(f"http://{self.hostname}/login")
+        user_input = driver.find_element(By.ID, "f_user")
+        user_input.send_keys("gerrit-admin")
+
+        pwd_input = driver.find_element(By.ID, "f_pass")
+        pwd_input.send_keys(self.ldap_credentials["gerrit-admin"])
+
+        submit_btn = driver.find_element(By.ID, "b_signin")
+        submit_btn.click()
+
+        driver.close()
+
+    def update(self):
+        with open(self.values_file, "w", encoding="UTF-8") as f:
+            yaml.dump(self.chart_opts, f)
+
+        self.cluster.helm.upgrade(
+            self.chart_path,
+            self.chart_name,
+            values_file=self.values_file,
+            fail_on_err=True,
+            namespace=self.namespace,
+        )
+
+    def wait_until_ready(self):
+        pod_labels = f"app=gerrit,release={self.chart_name}"
+        finished_in_time = self._wait_for_pod_readiness(
+            pod_labels, timeout=GERRIT_STARTUP_TIMEOUT
+        )
+
+        if not finished_in_time:
+            raise TimeOutException(
+                f"Gerrit pod was not ready in time ({GERRIT_STARTUP_TIMEOUT} s)."
+            )
+
+    def uninstall(self):
+        self.cluster.helm.delete(self.chart_name, namespace=self.namespace)
+        self.cluster.delete_namespace(self.namespace)
+
+    def set_gerrit_config_value(self, section, key, value):
+        if isinstance(self.gerrit_config[section][key], list):
+            self.gerrit_config[section][key].append(value)
+        else:
+            self.gerrit_config[section][key] = value
+        # pylint: disable=W1401
+        self.set_helm_value(
+            "gerrit.etc.config.gerrit\.config", dict_to_git_config(self.gerrit_config)
+        )
+
+    def _set_values_file(self):
+        return os.path.join(self.tmp_dir, "values.yaml")
+
+    def _configure_container_images(
+        self, container_registry, container_org, container_version
+    ):
+        self.set_helm_value("images.registry.name", container_registry)
+        self.set_helm_value("gitGC.image", f"{container_org}/git-gc")
+        self.set_helm_value("gerrit.images.gerritInit", f"{container_org}/gerrit-init")
+        self.set_helm_value("gerrit.images.gerrit", f"{container_org}/gerrit")
+        self.set_helm_value("images.version", container_version)
+
+    def _configure_ingress(self):
+        self.set_helm_value("ingress.enabled", True)
+        self.set_helm_value("ingress.host", self.hostname)
+
+    def _create_pvc(self):
+        core_v1 = client.CoreV1Api()
+        core_v1.create_namespaced_persistent_volume_claim(
+            self.namespace,
+            body=client.V1PersistentVolumeClaim(
+                kind="PersistentVolumeClaim",
+                api_version="v1",
+                metadata=client.V1ObjectMeta(name="repo-storage"),
+                spec=client.V1PersistentVolumeClaimSpec(
+                    access_modes=["ReadWriteMany"],
+                    storage_class_name=self.storageclass,
+                    resources=client.V1ResourceRequirements(
+                        requests={"storage": "1Gi"}
+                    ),
+                ),
+            ),
+        )
+
+
+@pytest.fixture(scope="class")
+def gerrit_deployment(
+    request, tmp_path_factory, test_cluster, ldap_admin_credentials, ldap_credentials
+):
+    deployment = GerritDeployment(
+        tmp_path_factory.mktemp("gerrit_deployment"),
+        test_cluster,
+        request.config.getoption("--rwm-storageclass").lower(),
+        request.config.getoption("--registry"),
+        request.config.getoption("--org"),
+        request.config.getoption("--tag"),
+        request.config.getoption("--ingress-url"),
+        ldap_admin_credentials,
+        ldap_credentials,
+    )
+
+    yield deployment
+
+    deployment.uninstall()
+
+
+@pytest.fixture(scope="class")
+def default_gerrit_deployment(gerrit_deployment):
+    gerrit_deployment.install()
+    gerrit_deployment.create_admin_account()
+
+    yield gerrit_deployment
diff --git a/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_plugins.py b/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_plugins.py
new file mode 100644
index 0000000..62981ac
--- /dev/null
+++ b/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_plugins.py
@@ -0,0 +1,343 @@
+# pylint: disable=W0613
+
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import hashlib
+import json
+import os.path
+import time
+
+import pytest
+import requests
+
+from kubernetes import client
+from kubernetes.stream import stream
+
+PLUGINS = ["avatars-gravatar", "readonly"]
+LIBS = ["global-refdb"]
+GERRIT_VERSION = "3.8"
+
+
+@pytest.fixture(scope="module")
+def plugin_list():
+    plugin_list = []
+    for plugin in PLUGINS:
+        url = (
+            f"https://gerrit-ci.gerritforge.com/view/Plugins-stable-{GERRIT_VERSION}/"
+            f"job/plugin-{plugin}-bazel-master-stable-{GERRIT_VERSION}/lastSuccessfulBuild/"
+            f"artifact/bazel-bin/plugins/{plugin}/{plugin}.jar"
+        )
+        jar = requests.get(url, verify=False).content
+        plugin_list.append(
+            {"name": plugin, "url": url, "sha1": hashlib.sha1(jar).hexdigest()}
+        )
+    return plugin_list
+
+
+@pytest.fixture(scope="module")
+def lib_list():
+    lib_list = []
+    for lib in LIBS:
+        url = (
+            f"https://gerrit-ci.gerritforge.com/view/Plugins-stable-{GERRIT_VERSION}/"
+            f"job/module-{lib}-bazel-stable-{GERRIT_VERSION}/lastSuccessfulBuild/"
+            f"artifact/bazel-bin/plugins/{lib}/{lib}.jar"
+        )
+        jar = requests.get(url, verify=False).content
+        lib_list.append(
+            {"name": lib, "url": url, "sha1": hashlib.sha1(jar).hexdigest()}
+        )
+    return lib_list
+
+
+@pytest.fixture(
+    scope="class",
+    params=[
+        [{"name": "replication"}],
+        [{"name": "replication"}, {"name": "download-commands"}],
+    ],
+    ids=["single-packaged-plugin", "multiple-packaged-plugins"],
+)
+def gerrit_deployment_with_packaged_plugins(request, gerrit_deployment):
+    gerrit_deployment.set_helm_value("gerrit.pluginManagement.plugins", request.param)
+    gerrit_deployment.install()
+    gerrit_deployment.create_admin_account()
+
+    yield gerrit_deployment, request.param
+
+
+@pytest.fixture(
+    scope="class", params=[1, 2], ids=["single-other-plugin", "multiple-other-plugins"]
+)
+def gerrit_deployment_with_other_plugins(
+    request,
+    plugin_list,
+    gerrit_deployment,
+):
+    selected_plugins = plugin_list[: request.param]
+
+    gerrit_deployment.set_helm_value(
+        "gerrit.pluginManagement.plugins", selected_plugins
+    )
+
+    gerrit_deployment.install()
+    gerrit_deployment.create_admin_account()
+
+    yield gerrit_deployment, selected_plugins
+
+
+@pytest.fixture(scope="class")
+def gerrit_deployment_with_libs(
+    request,
+    lib_list,
+    gerrit_deployment,
+):
+    gerrit_deployment.set_helm_value("gerrit.pluginManagement.libs", lib_list)
+
+    gerrit_deployment.install()
+    gerrit_deployment.create_admin_account()
+
+    yield gerrit_deployment, lib_list
+
+
+@pytest.fixture(scope="class")
+def gerrit_deployment_with_other_plugin_wrong_sha(plugin_list, gerrit_deployment):
+    plugin = plugin_list[0]
+    plugin["sha1"] = "notAValidSha"
+    gerrit_deployment.set_helm_value("gerrit.pluginManagement.plugins", [plugin])
+
+    gerrit_deployment.install(wait=False)
+
+    yield gerrit_deployment
+
+
+def get_gerrit_plugin_list(gerrit_url, user="admin", password="secret"):
+    list_plugins_url = f"{gerrit_url}/a/plugins/?all"
+    response = requests.get(list_plugins_url, auth=(user, password))
+    if not response.status_code == 200:
+        return None
+    body = response.text
+    return json.loads(body[body.index("\n") + 1 :])
+
+
+def get_gerrit_lib_list(gerrit_deployment):
+    response = (
+        stream(
+            client.CoreV1Api().connect_get_namespaced_pod_exec,
+            gerrit_deployment.chart_name + "-gerrit-stateful-set-0",
+            gerrit_deployment.namespace,
+            command=["/bin/ash", "-c", "ls /var/gerrit/lib"],
+            stdout=True,
+        )
+        .strip()
+        .split()
+    )
+    return [os.path.splitext(r)[0] for r in response]
+
+
+@pytest.mark.slow
+@pytest.mark.incremental
+@pytest.mark.integration
+@pytest.mark.kubernetes
+class TestgerritChartPackagedPluginInstall:
+    def _assert_installed_plugins(self, expected_plugins, installed_plugins):
+        for plugin in expected_plugins:
+            plugin_name = plugin["name"]
+            assert plugin_name in installed_plugins
+            assert installed_plugins[plugin_name]["filename"] == f"{plugin_name}.jar"
+
+    @pytest.mark.timeout(300)
+    def test_install_packaged_plugins(
+        self, request, gerrit_deployment_with_packaged_plugins, ldap_credentials
+    ):
+        gerrit_deployment, expected_plugins = gerrit_deployment_with_packaged_plugins
+        response = None
+        while not response:
+            try:
+                response = get_gerrit_plugin_list(
+                    f"http://{gerrit_deployment.hostname}",
+                    "gerrit-admin",
+                    ldap_credentials["gerrit-admin"],
+                )
+            except requests.exceptions.ConnectionError:
+                time.sleep(1)
+
+        self._assert_installed_plugins(expected_plugins, response)
+
+    @pytest.mark.timeout(300)
+    def test_install_packaged_plugins_are_removed_with_update(
+        self,
+        request,
+        test_cluster,
+        gerrit_deployment_with_packaged_plugins,
+        ldap_credentials,
+    ):
+        gerrit_deployment, expected_plugins = gerrit_deployment_with_packaged_plugins
+        removed_plugin = expected_plugins.pop()
+
+        gerrit_deployment.set_helm_value(
+            "gerrit.pluginManagement.plugins", expected_plugins
+        )
+        gerrit_deployment.update()
+
+        response = None
+        while True:
+            try:
+                response = get_gerrit_plugin_list(
+                    f"http://{gerrit_deployment.hostname}",
+                    "gerrit-admin",
+                    ldap_credentials["gerrit-admin"],
+                )
+                if response is not None and removed_plugin["name"] not in response:
+                    break
+            except requests.exceptions.ConnectionError:
+                time.sleep(1)
+
+        assert removed_plugin["name"] not in response
+        self._assert_installed_plugins(expected_plugins, response)
+
+
+@pytest.mark.slow
+@pytest.mark.incremental
+@pytest.mark.integration
+@pytest.mark.kubernetes
+class TestGerritChartOtherPluginInstall:
+    def _assert_installed_plugins(self, expected_plugins, installed_plugins):
+        for plugin in expected_plugins:
+            assert plugin["name"] in installed_plugins
+            assert (
+                installed_plugins[plugin["name"]]["filename"] == f"{plugin['name']}.jar"
+            )
+
+    @pytest.mark.timeout(300)
+    def test_install_other_plugins(
+        self, gerrit_deployment_with_other_plugins, ldap_credentials
+    ):
+        gerrit_deployment, expected_plugins = gerrit_deployment_with_other_plugins
+        response = None
+        while not response:
+            try:
+                response = get_gerrit_plugin_list(
+                    f"http://{gerrit_deployment.hostname}",
+                    "gerrit-admin",
+                    ldap_credentials["gerrit-admin"],
+                )
+            except requests.exceptions.ConnectionError:
+                continue
+        self._assert_installed_plugins(expected_plugins, response)
+
+    @pytest.mark.timeout(300)
+    def test_install_other_plugins_are_removed_with_update(
+        self, gerrit_deployment_with_other_plugins, ldap_credentials
+    ):
+        gerrit_deployment, installed_plugins = gerrit_deployment_with_other_plugins
+        removed_plugin = installed_plugins.pop()
+        gerrit_deployment.set_helm_value(
+            "gerrit.pluginManagement.plugins", installed_plugins
+        )
+        gerrit_deployment.update()
+
+        response = None
+        while True:
+            try:
+                response = get_gerrit_plugin_list(
+                    f"http://{gerrit_deployment.hostname}",
+                    "gerrit-admin",
+                    ldap_credentials["gerrit-admin"],
+                )
+                if response is not None and removed_plugin["name"] not in response:
+                    break
+            except requests.exceptions.ConnectionError:
+                time.sleep(1)
+
+        assert removed_plugin["name"] not in response
+        self._assert_installed_plugins(installed_plugins, response)
+
+
+@pytest.mark.slow
+@pytest.mark.incremental
+@pytest.mark.integration
+@pytest.mark.kubernetes
+class TestGerritChartLibModuleInstall:
+    def _assert_installed_libs(self, expected_libs, installed_libs):
+        for lib in expected_libs:
+            assert lib["name"] in installed_libs
+
+    @pytest.mark.timeout(300)
+    def test_install_libs(self, gerrit_deployment_with_libs):
+        gerrit_deployment, expected_libs = gerrit_deployment_with_libs
+        response = get_gerrit_lib_list(gerrit_deployment)
+        self._assert_installed_libs(expected_libs, response)
+
+    @pytest.mark.timeout(300)
+    def test_install_other_plugins_are_removed_with_update(
+        self, gerrit_deployment_with_libs
+    ):
+        gerrit_deployment, installed_libs = gerrit_deployment_with_libs
+        removed_lib = installed_libs.pop()
+        gerrit_deployment.set_helm_value("gerrit.pluginManagement.libs", installed_libs)
+        gerrit_deployment.update()
+
+        response = None
+        while True:
+            try:
+                response = get_gerrit_lib_list(gerrit_deployment)
+                if response is not None and removed_lib["name"] not in response:
+                    break
+            except requests.exceptions.ConnectionError:
+                time.sleep(1)
+
+        assert removed_lib["name"] not in response
+        self._assert_installed_libs(installed_libs, response)
+
+
+@pytest.mark.integration
+@pytest.mark.kubernetes
+@pytest.mark.timeout(180)
+def test_install_other_plugins_fails_wrong_sha(
+    gerrit_deployment_with_other_plugin_wrong_sha,
+):
+    pod_labels = f"app.kubernetes.io/component=gerrit,release={gerrit_deployment_with_other_plugin_wrong_sha.chart_name}"
+    core_v1 = client.CoreV1Api()
+    pod_name = ""
+    while not pod_name:
+        pod_list = core_v1.list_namespaced_pod(
+            namespace=gerrit_deployment_with_other_plugin_wrong_sha.namespace,
+            watch=False,
+            label_selector=pod_labels,
+        )
+        if len(pod_list.items) > 1:
+            raise RuntimeError("Too many gerrit pods with the same release name.")
+        elif len(pod_list.items) == 1:
+            pod_name = pod_list.items[0].metadata.name
+
+    current_status = None
+    while not current_status:
+        pod = core_v1.read_namespaced_pod_status(
+            pod_name, gerrit_deployment_with_other_plugin_wrong_sha.namespace
+        )
+        if not pod.status.init_container_statuses:
+            time.sleep(1)
+            continue
+        for init_container_status in pod.status.init_container_statuses:
+            if (
+                init_container_status.name == "gerrit-init"
+                and init_container_status.last_state.terminated
+            ):
+                current_status = init_container_status
+                assert current_status.last_state.terminated.exit_code > 0
+                return
+
+    assert current_status.last_state.terminated.exit_code > 0
diff --git a/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_setup.py b/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_setup.py
new file mode 100644
index 0000000..306d41c
--- /dev/null
+++ b/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_setup.py
@@ -0,0 +1,29 @@
+# pylint: disable=W0613
+
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+
+
+@pytest.mark.integration
+@pytest.mark.kubernetes
+def test_deployment(test_cluster, default_gerrit_deployment):
+    installed_charts = test_cluster.helm.list(default_gerrit_deployment.namespace)
+    gerrit_chart = None
+    for chart in installed_charts:
+        if chart["name"].startswith("gerrit"):
+            gerrit_chart = chart
+    assert gerrit_chart is not None
+    assert gerrit_chart["status"].lower() == "deployed"
diff --git a/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_smoke_test.py b/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_smoke_test.py
new file mode 100644
index 0000000..b3ee757
--- /dev/null
+++ b/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_smoke_test.py
@@ -0,0 +1,110 @@
+# pylint: disable=W0613
+
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os.path
+import re
+import shutil
+
+from pathlib import Path
+
+import pygit2 as git
+import pytest
+import requests
+
+import utils
+
+
+@pytest.fixture(scope="module")
+def admin_creds(request):
+    user = request.config.getoption("--gerrit-user")
+    pwd = request.config.getoption("--gerrit-pwd")
+    return user, pwd
+
+
+@pytest.fixture(scope="class")
+def tmp_test_repo(request, tmp_path_factory):
+    tmp_dir = tmp_path_factory.mktemp("gerrit_chart_clone_test")
+    yield tmp_dir
+    shutil.rmtree(tmp_dir)
+
+
+@pytest.fixture(scope="class")
+def random_repo_name():
+    return utils.create_random_string(16)
+
+
+@pytest.mark.smoke
+def test_ui_connection(request):
+    response = requests.get(request.config.getoption("--ingress-url"))
+    assert response.status_code == requests.codes["OK"]
+    assert re.search(r'content="Gerrit Code Review"', response.text)
+
+
+@pytest.mark.smoke
+@pytest.mark.incremental
+class TestGerritRestGitCalls:
+    def _is_delete_project_plugin_enabled(self, gerrit_url, user, pwd):
+        url = f"{gerrit_url}/a/plugins/delete-project/gerrit~status"
+        response = requests.get(url, auth=(user, pwd))
+        return response.status_code == requests.codes["OK"]
+
+    def test_create_project_rest(self, request, random_repo_name, admin_creds):
+        ingress_url = request.config.getoption("--ingress-url")
+        create_project_url = f"{ingress_url}/a/projects/{random_repo_name}"
+        response = requests.put(create_project_url, auth=admin_creds)
+        assert response.status_code == requests.codes["CREATED"]
+
+    def test_cloning_project(
+        self, request, tmp_test_repo, random_repo_name, admin_creds
+    ):
+        repo_url = f"{request.config.getoption('--ingress-url')}/{random_repo_name}.git"
+        repo_url = repo_url.replace("//", f"//{admin_creds[0]}:{admin_creds[1]}@")
+        repo = git.clone_repository(repo_url, tmp_test_repo)
+        assert repo.path == os.path.join(tmp_test_repo, ".git/")
+
+    def test_push_commit(self, tmp_test_repo):
+        repo = git.Repository(tmp_test_repo)
+        file_name = os.path.join(tmp_test_repo, "test.txt")
+        Path(file_name).touch()
+        repo.index.add("test.txt")
+        repo.index.write()
+        # pylint: disable=E1101
+        author = git.Signature("Gerrit Review", "gerrit@review.com")
+        committer = git.Signature("Gerrit Review", "gerrit@review.com")
+        message = "Initial commit"
+        tree = repo.index.write_tree()
+        repo.create_commit("HEAD", author, committer, message, tree, [])
+
+        origin = repo.remotes["origin"]
+        origin.push(["refs/heads/master:refs/heads/master"])
+
+        remote_refs = origin.ls_remotes()
+        assert remote_refs[0]["name"] == repo.revparse_single("HEAD").hex
+
+    def test_delete_project_rest(self, request, random_repo_name, admin_creds):
+        ingress_url = request.config.getoption("--ingress-url")
+        if not self._is_delete_project_plugin_enabled(
+            ingress_url, admin_creds[0], admin_creds[1]
+        ):
+            pytest.skip(
+                "Delete-project plugin not installed."
+                + f"The test project ({random_repo_name}) has to be deleted manually."
+            )
+        project_url = (
+            f"{ingress_url}/a/projects/{random_repo_name}/delete-project~delete"
+        )
+        response = requests.post(project_url, auth=admin_creds)
+        assert response.status_code == requests.codes["NO_CONTENT"]
diff --git a/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_ssl.py b/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_ssl.py
new file mode 100644
index 0000000..0eee0f4
--- /dev/null
+++ b/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_ssl.py
@@ -0,0 +1,89 @@
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os.path
+
+import pygit2 as git
+import pytest
+import requests
+
+import git_callbacks
+import mock_ssl
+
+
+@pytest.fixture(scope="module")
+def cert_dir(tmp_path_factory):
+    return tmp_path_factory.mktemp("gerrit-cert")
+
+
+def _create_ssl_certificate(url, cert_dir):
+    keypair = mock_ssl.MockSSLKeyPair("*." + url.split(".", 1)[1], url)
+    with open(os.path.join(cert_dir, "server.crt"), "wb") as f:
+        f.write(keypair.get_cert())
+    with open(os.path.join(cert_dir, "server.key"), "wb") as f:
+        f.write(keypair.get_key())
+    return keypair
+
+
+@pytest.fixture(scope="class")
+def gerrit_deployment_with_ssl(cert_dir, gerrit_deployment):
+    ssl_certificate = _create_ssl_certificate(gerrit_deployment.hostname, cert_dir)
+    gerrit_deployment.set_helm_value("ingress.tls.enabled", True)
+    gerrit_deployment.set_helm_value(
+        "ingress.tls.cert", ssl_certificate.get_cert().decode()
+    )
+    gerrit_deployment.set_helm_value(
+        "ingress.tls.key", ssl_certificate.get_key().decode()
+    )
+    gerrit_deployment.set_gerrit_config_value(
+        "httpd", "listenUrl", "proxy-https://*:8080/"
+    )
+    gerrit_deployment.set_gerrit_config_value(
+        "gerrit",
+        "canonicalWebUrl",
+        f"https://{gerrit_deployment.hostname}",
+    )
+
+    gerrit_deployment.install()
+    gerrit_deployment.create_admin_account()
+
+    yield gerrit_deployment
+
+
+@pytest.mark.incremental
+@pytest.mark.integration
+@pytest.mark.kubernetes
+@pytest.mark.slow
+class TestgerritChartSetup:
+    # pylint: disable=W0613
+    def test_create_project_rest(
+        self, cert_dir, gerrit_deployment_with_ssl, ldap_credentials
+    ):
+        create_project_url = (
+            f"https://{gerrit_deployment_with_ssl.hostname}/a/projects/test"
+        )
+        response = requests.put(
+            create_project_url,
+            auth=("gerrit-admin", ldap_credentials["gerrit-admin"]),
+            verify=os.path.join(cert_dir, "server.crt"),
+        )
+        assert response.status_code == 201
+
+    def test_cloning_project(self, tmp_path_factory, gerrit_deployment_with_ssl):
+        clone_dest = tmp_path_factory.mktemp("gerrit_chart_clone_test")
+        repo_url = f"https://{gerrit_deployment_with_ssl.hostname}/test.git"
+        repo = git.clone_repository(
+            repo_url, clone_dest, callbacks=git_callbacks.TestRemoteCallbacks()
+        )
+        assert repo.path == os.path.join(clone_dest, ".git/")
diff --git a/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_usage.py b/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_usage.py
new file mode 100644
index 0000000..f63d209
--- /dev/null
+++ b/charts/k8s-gerrit/tests/helm-charts/gerrit/test_chart_gerrit_usage.py
@@ -0,0 +1,51 @@
+# pylint: disable=W0613
+
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os.path
+
+import pygit2 as git
+import pytest
+import requests
+
+
+@pytest.mark.slow
+@pytest.mark.incremental
+@pytest.mark.integration
+@pytest.mark.kubernetes
+class TestGerritChartSetup:
+    @pytest.mark.timeout(240)
+    def test_create_project_rest(self, default_gerrit_deployment, ldap_credentials):
+        create_project_url = (
+            f"http://{default_gerrit_deployment.hostname}/a/projects/test"
+        )
+        response = None
+
+        while not response:
+            try:
+                response = requests.put(
+                    create_project_url,
+                    auth=("gerrit-admin", ldap_credentials["gerrit-admin"]),
+                )
+            except requests.exceptions.ConnectionError:
+                break
+
+        assert response.status_code == 201
+
+    def test_cloning_project(self, tmp_path_factory, default_gerrit_deployment):
+        clone_dest = tmp_path_factory.mktemp("gerrit_chart_clone_test")
+        repo_url = f"http://{default_gerrit_deployment.hostname}/test.git"
+        repo = git.clone_repository(repo_url, clone_dest)
+        assert repo.path == os.path.join(clone_dest, ".git/")
diff --git a/charts/k8s-gerrit/tests/helpers/git_callbacks.py b/charts/k8s-gerrit/tests/helpers/git_callbacks.py
new file mode 100644
index 0000000..3922a24
--- /dev/null
+++ b/charts/k8s-gerrit/tests/helpers/git_callbacks.py
@@ -0,0 +1,20 @@
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pygit2 as git
+
+
+class TestRemoteCallbacks(git.RemoteCallbacks):
+    def certificate_check(self, certificate, valid, host):
+        return True
diff --git a/charts/k8s-gerrit/tests/helpers/mock_ssl.py b/charts/k8s-gerrit/tests/helpers/mock_ssl.py
new file mode 100644
index 0000000..46d766c
--- /dev/null
+++ b/charts/k8s-gerrit/tests/helpers/mock_ssl.py
@@ -0,0 +1,49 @@
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from OpenSSL import crypto
+
+
+class MockSSLKeyPair:
+    def __init__(self, common_name, subject_alt_name):
+        self.common_name = common_name
+        self.subject_alt_name = subject_alt_name
+        self.cert = None
+        self.key = None
+
+        self._create_keypair()
+
+    def _create_keypair(self):
+        self.key = crypto.PKey()
+        self.key.generate_key(crypto.TYPE_RSA, 2048)
+
+        self.cert = crypto.X509()
+        self.cert.set_version(2)
+        self.cert.get_subject().O = "Gerrit"
+        self.cert.get_subject().CN = self.common_name
+        san = f"DNS:{self.subject_alt_name}"
+        self.cert.add_extensions(
+            [crypto.X509Extension(b"subjectAltName", False, san.encode())]
+        )
+        self.cert.gmtime_adj_notBefore(0)
+        self.cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60)
+        self.cert.set_issuer(self.cert.get_subject())
+        self.cert.set_pubkey(self.key)
+        self.cert.sign(self.key, "sha256")
+
+    def get_key(self):
+        return crypto.dump_privatekey(crypto.FILETYPE_PEM, self.key)
+
+    def get_cert(self):
+        return crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert)
diff --git a/charts/k8s-gerrit/tests/helpers/utils.py b/charts/k8s-gerrit/tests/helpers/utils.py
new file mode 100644
index 0000000..804217e
--- /dev/null
+++ b/charts/k8s-gerrit/tests/helpers/utils.py
@@ -0,0 +1,41 @@
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import random
+import string
+
+
+def check_if_ancestor_image_is_inherited(image, ancestor):
+    """Helper function that looks for a given ancestor image in the layers of a
+      provided image. It can be used to check, whether an image uses the expected
+      FROM-statement
+
+    Arguments:
+      image {docker.images.Image} -- Docker image object to be checked
+      ancestor {str} -- Complete name of the expected ancestor image
+
+    Returns:
+      boolean -- True, if ancestor is inherited by image
+    """
+
+    contains_tag = False
+    for layer in image.history():
+        contains_tag = layer["Tags"] is not None and ancestor in layer["Tags"]
+        if contains_tag:
+            break
+    return contains_tag
+
+
+def create_random_string(length=8):
+    return "".join([random.choice(string.ascii_letters) for n in range(length)]).lower()
diff --git a/charts/longhorn-1.4.1/.helmignore b/charts/longhorn-1.4.1/.helmignore
new file mode 100644
index 0000000..f0c1319
--- /dev/null
+++ b/charts/longhorn-1.4.1/.helmignore
@@ -0,0 +1,21 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
diff --git a/charts/longhorn-1.4.1/Chart.yaml b/charts/longhorn-1.4.1/Chart.yaml
new file mode 100644
index 0000000..d9dd5a1
--- /dev/null
+++ b/charts/longhorn-1.4.1/Chart.yaml
@@ -0,0 +1,28 @@
+apiVersion: v1
+appVersion: v1.4.1
+description: Longhorn is a distributed block storage system for Kubernetes.
+home: https://github.com/longhorn/longhorn
+icon: https://raw.githubusercontent.com/cncf/artwork/master/projects/longhorn/icon/color/longhorn-icon-color.png
+keywords:
+- longhorn
+- storage
+- distributed
+- block
+- device
+- iscsi
+- nfs
+kubeVersion: '>=1.21.0-0'
+maintainers:
+- email: maintainers@longhorn.io
+  name: Longhorn maintainers
+name: longhorn
+sources:
+- https://github.com/longhorn/longhorn
+- https://github.com/longhorn/longhorn-engine
+- https://github.com/longhorn/longhorn-instance-manager
+- https://github.com/longhorn/longhorn-share-manager
+- https://github.com/longhorn/longhorn-manager
+- https://github.com/longhorn/longhorn-ui
+- https://github.com/longhorn/longhorn-tests
+- https://github.com/longhorn/backing-image-manager
+version: 1.4.1
diff --git a/charts/longhorn-1.4.1/README.md b/charts/longhorn-1.4.1/README.md
new file mode 100644
index 0000000..012c058
--- /dev/null
+++ b/charts/longhorn-1.4.1/README.md
@@ -0,0 +1,78 @@
+# Longhorn Chart
+
+> **Important**: Please install the Longhorn chart in the `longhorn-system` namespace only.
+
+> **Warning**: Longhorn doesn't support downgrading from a higher version to a lower version.
+
+## Source Code
+
+Longhorn is 100% open source software. Project source code is spread across a number of repos:
+
+1. Longhorn Engine -- Core controller/replica logic https://github.com/longhorn/longhorn-engine
+2. Longhorn Instance Manager -- Controller/replica instance lifecycle management https://github.com/longhorn/longhorn-instance-manager
+3. Longhorn Share Manager -- NFS provisioner that exposes Longhorn volumes as ReadWriteMany volumes. https://github.com/longhorn/longhorn-share-manager
+4. Backing Image Manager -- Backing image file lifecycle management. https://github.com/longhorn/backing-image-manager
+5. Longhorn Manager -- Longhorn orchestration, includes CSI driver for Kubernetes https://github.com/longhorn/longhorn-manager
+6. Longhorn UI -- Dashboard https://github.com/longhorn/longhorn-ui
+
+## Prerequisites
+
+1. A container runtime compatible with Kubernetes (Docker v1.13+, containerd v1.3.7+, etc.)
+2. Kubernetes >= v1.21
+3. Make sure `bash`, `curl`, `findmnt`, `grep`, `awk` and `blkid` has been installed in all nodes of the Kubernetes cluster.
+4. Make sure `open-iscsi` has been installed, and the `iscsid` daemon is running on all nodes of the Kubernetes cluster. For GKE, recommended Ubuntu as guest OS image since it contains `open-iscsi` already.
+
+## Upgrading to Kubernetes v1.25+
+
+Starting in Kubernetes v1.25, [Pod Security Policies](https://kubernetes.io/docs/concepts/security/pod-security-policy/) have been removed from the Kubernetes API.
+
+As a result, **before upgrading to Kubernetes v1.25** (or on a fresh install in a Kubernetes v1.25+ cluster), users are expected to perform an in-place upgrade of this chart with `enablePSP` set to `false` if it has been previously set to `true`.
+
+> **Note:**
+> If you upgrade your cluster to Kubernetes v1.25+ before removing PSPs via a `helm upgrade` (even if you manually clean up resources), **it will leave the Helm release in a broken state within the cluster such that further Helm operations will not work (`helm uninstall`, `helm upgrade`, etc.).**
+>
+> If your charts get stuck in this state, you may have to clean up your Helm release secrets.
+Upon setting `enablePSP` to false, the chart will remove any PSP resources deployed on its behalf from the cluster. This is the default setting for this chart.
+
+As a replacement for PSPs, [Pod Security Admission](https://kubernetes.io/docs/concepts/security/pod-security-admission/) should be used. Please consult the Longhorn docs for more details on how to configure your chart release namespaces to work with the new Pod Security Admission and apply Pod Security Standards.
+
+## Installation
+1. Add Longhorn chart repository.
+```
+helm repo add longhorn https://charts.longhorn.io
+```
+
+2. Update local Longhorn chart information from chart repository.
+```
+helm repo update
+```
+
+3. Install Longhorn chart.
+- With Helm 2, the following command will create the `longhorn-system` namespace and install the Longhorn chart together.
+```
+helm install longhorn/longhorn --name longhorn --namespace longhorn-system
+``` 
+- With Helm 3, the following commands will create the `longhorn-system` namespace first, then install the Longhorn chart.
+
+```
+kubectl create namespace longhorn-system
+helm install longhorn longhorn/longhorn --namespace longhorn-system
+```
+
+## Uninstallation
+
+With Helm 2 to uninstall Longhorn.
+```
+kubectl -n longhorn-system patch -p '{"value": "true"}' --type=merge lhs deleting-confirmation-flag
+helm delete longhorn --purge
+```
+
+With Helm 3 to uninstall Longhorn.
+```
+kubectl -n longhorn-system patch -p '{"value": "true"}' --type=merge lhs deleting-confirmation-flag
+helm uninstall longhorn -n longhorn-system
+kubectl delete namespace longhorn-system
+```
+
+---
+Please see [link](https://github.com/longhorn/longhorn) for more information.
diff --git a/charts/longhorn-1.4.1/app-readme.md b/charts/longhorn-1.4.1/app-readme.md
new file mode 100644
index 0000000..cb23135
--- /dev/null
+++ b/charts/longhorn-1.4.1/app-readme.md
@@ -0,0 +1,11 @@
+# Longhorn
+
+Longhorn is a lightweight, reliable and easy to use distributed block storage system for Kubernetes. Once deployed, users can leverage persistent volumes provided by Longhorn.
+
+Longhorn creates a dedicated storage controller for each volume and synchronously replicates the volume across multiple replicas stored on multiple nodes. The storage controller and replicas are themselves orchestrated using Kubernetes. Longhorn supports snapshots, backups and even allows you to schedule recurring snapshots and backups!
+
+**Important**: Please install Longhorn chart in `longhorn-system` namespace only.
+
+**Warning**: Longhorn doesn't support downgrading from a higher version to a lower version.
+
+[Chart Documentation](https://github.com/longhorn/longhorn/blob/master/chart/README.md)
diff --git a/charts/longhorn-1.4.1/questions.yaml b/charts/longhorn-1.4.1/questions.yaml
new file mode 100644
index 0000000..b4ae9de
--- /dev/null
+++ b/charts/longhorn-1.4.1/questions.yaml
@@ -0,0 +1,837 @@
+categories:
+- storage
+namespace: longhorn-system
+questions:
+- variable: image.defaultImage
+  default: "true"
+  description: "Use default Longhorn images"
+  label: Use Default Images
+  type: boolean
+  show_subquestion_if: false
+  group: "Longhorn Images"
+  subquestions:
+  - variable: image.longhorn.manager.repository
+    default: longhornio/longhorn-manager
+    description: "Specify Longhorn Manager Image Repository"
+    type: string
+    label: Longhorn Manager Image Repository
+    group: "Longhorn Images Settings"
+  - variable: image.longhorn.manager.tag
+    default: v1.4.1
+    description: "Specify Longhorn Manager Image Tag"
+    type: string
+    label: Longhorn Manager Image Tag
+    group: "Longhorn Images Settings"
+  - variable: image.longhorn.engine.repository
+    default: longhornio/longhorn-engine
+    description: "Specify Longhorn Engine Image Repository"
+    type: string
+    label: Longhorn Engine Image Repository
+    group: "Longhorn Images Settings"
+  - variable: image.longhorn.engine.tag
+    default: v1.4.1
+    description: "Specify Longhorn Engine Image Tag"
+    type: string
+    label: Longhorn Engine Image Tag
+    group: "Longhorn Images Settings"
+  - variable: image.longhorn.ui.repository
+    default:  longhornio/longhorn-ui
+    description: "Specify Longhorn UI Image Repository"
+    type: string
+    label: Longhorn UI Image Repository
+    group: "Longhorn Images Settings"
+  - variable: image.longhorn.ui.tag
+    default: v1.4.1
+    description: "Specify Longhorn UI Image Tag"
+    type: string
+    label: Longhorn UI Image Tag
+    group: "Longhorn Images Settings"
+  - variable: image.longhorn.instanceManager.repository
+    default: longhornio/longhorn-instance-manager
+    description: "Specify Longhorn Instance Manager Image Repository"
+    type: string
+    label: Longhorn Instance Manager Image Repository
+    group: "Longhorn Images Settings"
+  - variable: image.longhorn.instanceManager.tag
+    default: v1.4.1
+    description: "Specify Longhorn Instance Manager Image Tag"
+    type: string
+    label: Longhorn Instance Manager Image Tag
+    group: "Longhorn Images Settings"
+  - variable: image.longhorn.shareManager.repository
+    default: longhornio/longhorn-share-manager
+    description: "Specify Longhorn Share Manager Image Repository"
+    type: string
+    label: Longhorn Share Manager Image Repository
+    group: "Longhorn Images Settings"
+  - variable: image.longhorn.shareManager.tag
+    default: v1.4.1
+    description: "Specify Longhorn Share Manager Image Tag"
+    type: string
+    label: Longhorn Share Manager Image Tag
+    group: "Longhorn Images Settings"
+  - variable: image.longhorn.backingImageManager.repository
+    default: longhornio/backing-image-manager
+    description: "Specify Longhorn Backing Image Manager Image Repository"
+    type: string
+    label: Longhorn Backing Image Manager Image Repository
+    group: "Longhorn Images Settings"
+  - variable: image.longhorn.backingImageManager.tag
+    default: v1.4.1
+    description: "Specify Longhorn Backing Image Manager Image Tag"
+    type: string
+    label: Longhorn Backing Image Manager Image Tag
+    group: "Longhorn Images Settings"
+  - variable: image.longhorn.supportBundleKit.repository
+    default: longhornio/support-bundle-kit
+    description: "Specify Longhorn Support Bundle Manager Image Repository"
+    type: string
+    label: Longhorn Support Bundle Kit Image Repository
+    group: "Longhorn Images Settings"
+  - variable: image.longhorn.supportBundleKit.tag
+    default: v0.0.17
+    description: "Specify Longhorn Support Bundle Manager Image Tag"
+    type: string
+    label: Longhorn Support Bundle Kit Image Tag
+    group: "Longhorn Images Settings"
+  - variable: image.csi.attacher.repository
+    default: longhornio/csi-attacher
+    description: "Specify CSI attacher image repository. Leave blank to autodetect."
+    type: string
+    label: Longhorn CSI Attacher Image Repository
+    group: "Longhorn CSI Driver Images"
+  - variable: image.csi.attacher.tag
+    default: v3.4.0
+    description: "Specify CSI attacher image tag. Leave blank to autodetect."
+    type: string
+    label: Longhorn CSI Attacher Image Tag
+    group: "Longhorn CSI Driver Images"
+  - variable: image.csi.provisioner.repository
+    default: longhornio/csi-provisioner
+    description: "Specify CSI provisioner image repository. Leave blank to autodetect."
+    type: string
+    label: Longhorn CSI Provisioner Image Repository
+    group: "Longhorn CSI Driver Images"
+  - variable: image.csi.provisioner.tag
+    default: v2.1.2
+    description: "Specify CSI provisioner image tag. Leave blank to autodetect."
+    type: string
+    label: Longhorn CSI Provisioner Image Tag
+    group: "Longhorn CSI Driver Images"
+  - variable: image.csi.nodeDriverRegistrar.repository
+    default: longhornio/csi-node-driver-registrar
+    description: "Specify CSI Node Driver Registrar image repository. Leave blank to autodetect."
+    type: string
+    label: Longhorn CSI Node Driver Registrar Image Repository
+    group: "Longhorn CSI Driver Images"
+  - variable: image.csi.nodeDriverRegistrar.tag
+    default: v2.5.0
+    description: "Specify CSI Node Driver Registrar image tag. Leave blank to autodetect."
+    type: string
+    label: Longhorn CSI Node Driver Registrar Image Tag
+    group: "Longhorn CSI Driver Images"
+  - variable: image.csi.resizer.repository
+    default: longhornio/csi-resizer
+    description: "Specify CSI Driver Resizer image repository. Leave blank to autodetect."
+    type: string
+    label: Longhorn CSI Driver Resizer Image Repository
+    group: "Longhorn CSI Driver Images"
+  - variable: image.csi.resizer.tag
+    default: v1.3.0
+    description: "Specify CSI Driver Resizer image tag. Leave blank to autodetect."
+    type: string
+    label: Longhorn CSI Driver Resizer Image Tag
+    group: "Longhorn CSI Driver Images"
+  - variable: image.csi.snapshotter.repository
+    default: longhornio/csi-snapshotter
+    description: "Specify CSI Driver Snapshotter image repository. Leave blank to autodetect."
+    type: string
+    label: Longhorn CSI Driver Snapshotter Image Repository
+    group: "Longhorn CSI Driver Images"
+  - variable: image.csi.snapshotter.tag
+    default: v5.0.1
+    description: "Specify CSI Driver Snapshotter image tag. Leave blank to autodetect."
+    type: string
+    label: Longhorn CSI Driver Snapshotter Image Tag
+    group: "Longhorn CSI Driver Images"
+  - variable: image.csi.livenessProbe.repository
+    default: longhornio/livenessprobe
+    description: "Specify CSI liveness probe image repository. Leave blank to autodetect."
+    type: string
+    label: Longhorn CSI Liveness Probe Image Repository
+    group: "Longhorn CSI Driver Images"
+  - variable: image.csi.livenessProbe.tag
+    default: v2.8.0
+    description: "Specify CSI liveness probe image tag. Leave blank to autodetect."
+    type: string
+    label: Longhorn CSI Liveness Probe Image Tag
+    group: "Longhorn CSI Driver Images"
+- variable: privateRegistry.registryUrl
+  label: Private registry URL
+  description: "URL of private registry. Leave blank to apply system default registry."
+  group: "Private Registry Settings"
+  type: string
+  default: ""
+- variable: privateRegistry.registrySecret
+  label: Private registry secret name
+  description: "If create a new private registry secret is true, create a Kubernetes secret with this name; else use the existing secret of this name. Use it to pull images from your private registry."
+  group: "Private Registry Settings"
+  type: string
+  default: ""
+- variable: privateRegistry.createSecret
+  default: "true"
+  description: "Create a new private registry secret"
+  type: boolean
+  group: "Private Registry Settings"
+  label: Create Secret for Private Registry Settings
+  show_subquestion_if: true
+  subquestions:
+  - variable: privateRegistry.registryUser
+    label: Private registry user
+    description: "User used to authenticate to private registry."
+    type: string
+    default: ""
+  - variable: privateRegistry.registryPasswd
+    label: Private registry password
+    description: "Password used to authenticate to private registry."
+    type: password
+    default: ""
+- variable: longhorn.default_setting
+  default: "false"
+  description: "Customize the default settings before installing Longhorn for the first time. This option will only work if the cluster hasn't installed Longhorn."
+  label: "Customize Default Settings"
+  type: boolean
+  show_subquestion_if: true
+  group: "Longhorn Default Settings"
+  subquestions:
+  - variable: csi.kubeletRootDir
+    default:
+    description: "Specify kubelet root-dir. Leave blank to autodetect."
+    type: string
+    label: Kubelet Root Directory
+    group: "Longhorn CSI Driver Settings"
+  - variable: csi.attacherReplicaCount
+    type: int
+    default: 3
+    min: 1
+    max: 10
+    description: "Specify replica count of CSI Attacher. By default 3."
+    label: Longhorn CSI Attacher replica count
+    group: "Longhorn CSI Driver Settings"
+  - variable: csi.provisionerReplicaCount
+    type: int
+    default: 3
+    min: 1
+    max: 10
+    description: "Specify replica count of CSI Provisioner. By default 3."
+    label: Longhorn CSI Provisioner replica count
+    group: "Longhorn CSI Driver Settings"
+  - variable: csi.resizerReplicaCount
+    type: int
+    default: 3
+    min: 1
+    max: 10
+    description: "Specify replica count of CSI Resizer. By default 3."
+    label: Longhorn CSI Resizer replica count
+    group: "Longhorn CSI Driver Settings"
+  - variable: csi.snapshotterReplicaCount
+    type: int
+    default: 3
+    min: 1
+    max: 10
+    description: "Specify replica count of CSI Snapshotter. By default 3."
+    label: Longhorn CSI Snapshotter replica count
+    group: "Longhorn CSI Driver Settings"
+  - variable: defaultSettings.backupTarget
+    label: Backup Target
+    description: "The endpoint used to access the backupstore. NFS and S3 are supported."
+    group: "Longhorn Default Settings"
+    type: string
+    default:
+  - variable: defaultSettings.backupTargetCredentialSecret
+    label: Backup Target Credential Secret
+    description: "The name of the Kubernetes secret associated with the backup target."
+    group: "Longhorn Default Settings"
+    type: string
+    default:
+  - variable: defaultSettings.allowRecurringJobWhileVolumeDetached
+    label: Allow Recurring Job While Volume Is Detached
+    description: 'If this setting is enabled, Longhorn will automatically attaches the volume and takes snapshot/backup when it is the time to do recurring snapshot/backup.
+Note that the volume is not ready for workload during the period when the volume was automatically attached. Workload will have to wait until the recurring job finishes.'
+    group: "Longhorn Default Settings"
+    type: boolean
+    default: "false"
+  - variable: defaultSettings.createDefaultDiskLabeledNodes
+    label: Create Default Disk on Labeled Nodes
+    description: 'Create default Disk automatically only on Nodes with the label "node.longhorn.io/create-default-disk=true" if no other disks exist. If disabled, the default disk will be created on all new nodes when each node is first added.'
+    group: "Longhorn Default Settings"
+    type: boolean
+    default: "false"
+  - variable: defaultSettings.defaultDataPath
+    label: Default Data Path
+    description: 'Default path to use for storing data on a host. By default "/var/lib/longhorn/"'
+    group: "Longhorn Default Settings"
+    type: string
+    default: "/var/lib/longhorn/"
+  - variable: defaultSettings.defaultDataLocality
+    label: Default Data Locality
+    description: 'We say a Longhorn volume has data locality if there is a local replica of the volume on the same node as the pod which is using the volume.
+This setting specifies the default data locality when a volume is created from the Longhorn UI. For Kubernetes configuration, update the `dataLocality` in the StorageClass
+The available modes are:
+- **disabled**. This is the default option. There may or may not be a replica on the same node as the attached volume (workload)
+- **best-effort**. This option instructs Longhorn to try to keep a replica on the same node as the attached volume (workload). Longhorn will not stop the volume, even if it cannot keep a replica local to the attached volume (workload) due to environment limitation, e.g. not enough disk space, incompatible disk tags, etc.'
+    group: "Longhorn Default Settings"
+    type: enum
+    options:
+    - "disabled"
+    - "best-effort"
+    default: "disabled"
+  - variable: defaultSettings.replicaSoftAntiAffinity
+    label: Replica Node Level Soft Anti-Affinity
+    description: 'Allow scheduling on nodes with existing healthy replicas of the same volume. By default false.'
+    group: "Longhorn Default Settings"
+    type: boolean
+    default: "false"
+  - variable: defaultSettings.replicaAutoBalance
+    label: Replica Auto Balance
+    description: 'Enable this setting automatically rebalances replicas when discovered an available node.
+The available global options are:
+- **disabled**. This is the default option. No replica auto-balance will be done.
+- **least-effort**. This option instructs Longhorn to balance replicas for minimal redundancy.
+- **best-effort**. This option instructs Longhorn to balance replicas for even redundancy.
+Longhorn also support individual volume setting. The setting can be specified in volume.spec.replicaAutoBalance, this overrules the global setting.
+The available volume spec options are:
+- **ignored**. This is the default option that instructs Longhorn to inherit from the global setting.
+- **disabled**. This option instructs Longhorn no replica auto-balance should be done.
+- **least-effort**. This option instructs Longhorn to balance replicas for minimal redundancy.
+- **best-effort**. This option instructs Longhorn to balance replicas for even redundancy.'
+    group: "Longhorn Default Settings"
+    type: enum
+    options:
+    - "disabled"
+    - "least-effort"
+    - "best-effort"
+    default: "disabled"
+  - variable: defaultSettings.storageOverProvisioningPercentage
+    label: Storage Over Provisioning Percentage
+    description: "The over-provisioning percentage defines how much storage can be allocated relative to the hard drive's capacity. By default 200."
+    group: "Longhorn Default Settings"
+    type: int
+    min: 0
+    default: 200
+  - variable: defaultSettings.storageMinimalAvailablePercentage
+    label: Storage Minimal Available Percentage
+    description: "If the minimum available disk capacity exceeds the actual percentage of available disk capacity, the disk becomes unschedulable until more space is freed up. By default 25."
+    group: "Longhorn Default Settings"
+    type: int
+    min: 0
+    max: 100
+    default: 25
+  - variable: defaultSettings.upgradeChecker
+    label: Enable Upgrade Checker
+    description: 'Upgrade Checker will check for new Longhorn version periodically. When there is a new version available, a notification will appear in the UI. By default true.'
+    group: "Longhorn Default Settings"
+    type: boolean
+    default: "true"
+  - variable: defaultSettings.defaultReplicaCount
+    label: Default Replica Count
+    description: "The default number of replicas when a volume is created from the Longhorn UI. For Kubernetes configuration, update the `numberOfReplicas` in the StorageClass. By default 3."
+    group: "Longhorn Default Settings"
+    type: int
+    min: 1
+    max: 20
+    default: 3
+  - variable: defaultSettings.defaultLonghornStaticStorageClass
+    label: Default Longhorn Static StorageClass Name
+    description: "The 'storageClassName' is given to PVs and PVCs that are created for an existing Longhorn volume. The StorageClass name can also be used as a label, so it is possible to use a Longhorn StorageClass to bind a workload to an existing PV without creating a Kubernetes StorageClass object. By default 'longhorn-static'."
+    group: "Longhorn Default Settings"
+    type: string
+    default: "longhorn-static"
+  - variable: defaultSettings.backupstorePollInterval
+    label: Backupstore Poll Interval
+    description: "In seconds. The backupstore poll interval determines how often Longhorn checks the backupstore for new backups. Set to 0 to disable the polling. By default 300."
+    group: "Longhorn Default Settings"
+    type: int
+    min: 0
+    default: 300
+  - variable: defaultSettings.failedBackupTTL
+    label: Failed Backup Time to Live
+    description: "In minutes. This setting determines how long Longhorn will keep the backup resource that was failed. Set to 0 to disable the auto-deletion.
+Failed backups will be checked and cleaned up during backupstore polling which is controlled by **Backupstore Poll Interval** setting.
+Hence this value determines the minimal wait interval of the cleanup. And the actual cleanup interval is multiple of **Backupstore Poll Interval**.
+Disabling **Backupstore Poll Interval** also means to disable failed backup auto-deletion."
+    group: "Longhorn Default Settings"
+    type: int
+    min: 0
+    default: 1440
+  - variable: defaultSettings.restoreVolumeRecurringJobs
+    label: Restore Volume Recurring Jobs
+    description: "Restore recurring jobs from the backup volume on the backup target and create recurring jobs if not exist during a backup restoration.
+Longhorn also supports individual volume setting. The setting can be specified on Backup page when making a backup restoration, this overrules the global setting.
+The available volume setting options are:
+- **ignored**. This is the default option that instructs Longhorn to inherit from the global setting.
+- **enabled**. This option instructs Longhorn to restore recurring jobs/groups from the backup target forcibly.
+- **disabled**. This option instructs Longhorn no restoring recurring jobs/groups should be done."
+    group: "Longhorn Default Settings"
+    type: boolean
+    default: "false"
+  - variable: defaultSettings.recurringSuccessfulJobsHistoryLimit
+    label: Cronjob Successful Jobs History Limit
+    description: "This setting specifies how many successful backup or snapshot job histories should be retained. History will not be retained if the value is 0."
+    group: "Longhorn Default Settings"
+    type: int
+    min: 0
+    default: 1
+  - variable: defaultSettings.recurringFailedJobsHistoryLimit
+    label: Cronjob Failed Jobs History Limit
+    description: "This setting specifies how many failed backup or snapshot job histories should be retained. History will not be retained if the value is 0."
+    group: "Longhorn Default Settings"
+    type: int
+    min: 0
+    default: 1
+  - variable: defaultSettings.supportBundleFailedHistoryLimit
+    label: SupportBundle Failed History Limit
+    description: "This setting specifies how many failed support bundles can exist in the cluster.
+The retained failed support bundle is for analysis purposes and needs to clean up manually.
+Set this value to **0** to have Longhorn automatically purge all failed support bundles."
+    group: "Longhorn Default Settings"
+    type: int
+    min: 0
+    default: 1
+  - variable: defaultSettings.autoSalvage
+    label: Automatic salvage
+    description: "If enabled, volumes will be automatically salvaged when all the replicas become faulty e.g. due to network disconnection. Longhorn will try to figure out which replica(s) are usable, then use them for the volume. By default true."
+    group: "Longhorn Default Settings"
+    type: boolean
+    default: "true"
+  - variable: defaultSettings.autoDeletePodWhenVolumeDetachedUnexpectedly
+    label: Automatically Delete Workload Pod when The Volume Is Detached Unexpectedly
+    description: 'If enabled, Longhorn will automatically delete the workload pod that is managed by a controller (e.g. deployment, statefulset, daemonset, etc...) when Longhorn volume is detached unexpectedly (e.g. during Kubernetes upgrade, Docker reboot, or network disconnect). By deleting the pod, its controller restarts the pod and Kubernetes handles volume reattachment and remount.
+If disabled, Longhorn will not delete the workload pod that is managed by a controller. You will have to manually restart the pod to reattach and remount the volume.
+**Note:** This setting does not apply to the workload pods that do not have a controller. Longhorn never deletes them.'
+    group: "Longhorn Default Settings"
+    type: boolean
+    default: "true"
+  - variable: defaultSettings.disableSchedulingOnCordonedNode
+    label: Disable Scheduling On Cordoned Node
+    description: "Disable Longhorn manager to schedule replica on Kubernetes cordoned node. By default true."
+    group: "Longhorn Default Settings"
+    type: boolean
+    default: "true"
+  - variable: defaultSettings.replicaZoneSoftAntiAffinity
+    label: Replica Zone Level Soft Anti-Affinity
+    description: "Allow scheduling new Replicas of Volume to the Nodes in the same Zone as existing healthy Replicas. Nodes don't belong to any Zone will be treated as in the same Zone. Notice that Longhorn relies on label `topology.kubernetes.io/zone=<Zone name of the node>` in the Kubernetes node object to identify the zone. By default true."
+    group: "Longhorn Default Settings"
+    type: boolean
+    default: "true"
+  - variable: defaultSettings.nodeDownPodDeletionPolicy
+    label: Pod Deletion Policy When Node is Down
+    description: "Defines the Longhorn action when a Volume is stuck with a StatefulSet/Deployment Pod on a node that is down.
+- **do-nothing** is the default Kubernetes behavior of never force deleting StatefulSet/Deployment terminating pods. Since the pod on the node that is down isn't removed, Longhorn volumes are stuck on nodes that are down.
+- **delete-statefulset-pod** Longhorn will force delete StatefulSet terminating pods on nodes that are down to release Longhorn volumes so that Kubernetes can spin up replacement pods.
+- **delete-deployment-pod** Longhorn will force delete Deployment terminating pods on nodes that are down to release Longhorn volumes so that Kubernetes can spin up replacement pods.
+- **delete-both-statefulset-and-deployment-pod** Longhorn will force delete StatefulSet/Deployment terminating pods on nodes that are down to release Longhorn volumes so that Kubernetes can spin up replacement pods."
+    group: "Longhorn Default Settings"
+    type: enum
+    options:
+    - "do-nothing"
+    - "delete-statefulset-pod"
+    - "delete-deployment-pod"
+    - "delete-both-statefulset-and-deployment-pod"
+    default: "do-nothing"
+  - variable: defaultSettings.allowNodeDrainWithLastHealthyReplica
+    label: Allow Node Drain with the Last Healthy Replica
+    description: "By default, Longhorn will block `kubectl drain` action on a node if the node contains the last healthy replica of a volume.
+If this setting is enabled, Longhorn will **not** block `kubectl drain` action on a node even if the node contains the last healthy replica of a volume."
+    group: "Longhorn Default Settings"
+    type: boolean
+    default: "false"
+  - variable: defaultSettings.mkfsExt4Parameters
+    label: Custom mkfs.ext4 parameters
+    description: "Allows setting additional filesystem creation parameters for ext4. For older host kernels it might be necessary to disable the optional ext4 metadata_csum feature by specifying `-O ^64bit,^metadata_csum`."
+    group: "Longhorn Default Settings"
+    type: string
+  - variable: defaultSettings.disableReplicaRebuild
+    label: Disable Replica Rebuild
+    description: "This setting disable replica rebuild cross the whole cluster, eviction and data locality feature won't work if this setting is true. But doesn't have any impact to any current replica rebuild and restore disaster recovery volume."
+    group: "Longhorn Default Settings"
+    type: boolean
+    default: "false"
+  - variable: defaultSettings.replicaReplenishmentWaitInterval
+    label: Replica Replenishment Wait Interval
+    description: "In seconds. The interval determines how long Longhorn will wait at least in order to reuse the existing data on a failed replica rather than directly creating a new replica for a degraded volume.
+Warning: This option works only when there is a failed replica in the volume. And this option may block the rebuilding for a while in the case."
+    group: "Longhorn Default Settings"
+    type: int
+    min: 0
+    default: 600
+  - variable: defaultSettings.concurrentReplicaRebuildPerNodeLimit
+    label: Concurrent Replica Rebuild Per Node Limit
+    description: "This setting controls how many replicas on a node can be rebuilt simultaneously.
+Typically, Longhorn can block the replica starting once the current rebuilding count on a node exceeds the limit. But when the value is 0, it means disabling the replica rebuilding.
+WARNING:
+- The old setting \"Disable Replica Rebuild\" is replaced by this setting.
+- Different from relying on replica starting delay to limit the concurrent rebuilding, if the rebuilding is disabled, replica object replenishment will be directly skipped.
+- When the value is 0, the eviction and data locality feature won't work. But this shouldn't have any impact to any current replica rebuild and backup restore."
+    group: "Longhorn Default Settings"
+    type: int
+    min: 0
+    default: 5
+  - variable: defaultSettings.concurrentVolumeBackupRestorePerNodeLimit
+    label: Concurrent Volume Backup Restore Per Node Limit
+    description: "This setting controls how many volumes on a node can restore the backup concurrently.
+Longhorn blocks the backup restore once the restoring volume count exceeds the limit.
+Set the value to **0** to disable backup restore."
+    group: "Longhorn Default Settings"
+    type: int
+    min: 0
+    default: 5
+  - variable: defaultSettings.disableRevisionCounter
+    label: Disable Revision Counter
+    description: "This setting is only for volumes created by UI. By default, this is false meaning there will be a reivision counter file to track every write to the volume. During salvage recovering Longhorn will pick the replica with largest reivision counter as candidate to recover the whole volume. If revision counter is disabled, Longhorn will not track every write to the volume. During the salvage recovering, Longhorn will use the 'volume-head-xxx.img' file last modification time and file size to pick the replica candidate to recover the whole volume."
+    group: "Longhorn Default Settings"
+    type: boolean
+    default: "false"
+  - variable: defaultSettings.systemManagedPodsImagePullPolicy
+    label: System Managed Pod Image Pull Policy
+    description: "This setting defines the Image Pull Policy of Longhorn system managed pods, e.g. instance manager, engine image, CSI driver, etc. The new Image Pull Policy will only apply after the system managed pods restart."
+    group: "Longhorn Default Settings"
+    type: enum
+    options:
+    - "if-not-present"
+    - "always"
+    - "never"
+    default: "if-not-present"
+  - variable: defaultSettings.allowVolumeCreationWithDegradedAvailability
+    label: Allow Volume Creation with Degraded Availability
+    description: "This setting allows user to create and attach a volume that doesn't have all the replicas scheduled at the time of creation."
+    group: "Longhorn Default Settings"
+    type: boolean
+    default: "true"
+  - variable: defaultSettings.autoCleanupSystemGeneratedSnapshot
+    label: Automatically Cleanup System Generated Snapshot
+    description: "This setting enables Longhorn to automatically cleanup the system generated snapshot after replica rebuild is done."
+    group: "Longhorn Default Settings"
+    type: boolean
+    default: "true"
+  - variable: defaultSettings.concurrentAutomaticEngineUpgradePerNodeLimit
+    label: Concurrent Automatic Engine Upgrade Per Node Limit
+    description: "This setting controls how Longhorn automatically upgrades volumes' engines to the new default engine image after upgrading Longhorn manager. The value of this setting specifies the maximum number of engines per node that are allowed to upgrade to the default engine image at the same time. If the value is 0, Longhorn will not automatically upgrade volumes' engines to default version."
+    group: "Longhorn Default Settings"
+    type: int
+    min: 0
+    default: 0
+  - variable: defaultSettings.backingImageCleanupWaitInterval
+    label: Backing Image Cleanup Wait Interval
+    description: "This interval in minutes determines how long Longhorn will wait before cleaning up the backing image file when there is no replica in the disk using it."
+    group: "Longhorn Default Settings"
+    type: int
+    min: 0
+    default: 60
+  - variable: defaultSettings.backingImageRecoveryWaitInterval
+    label: Backing Image Recovery Wait Interval
+    description: "This interval in seconds determines how long Longhorn will wait before re-downloading the backing image file when all disk files of this backing image become failed or unknown.
+    WARNING:
+      - This recovery only works for the backing image of which the creation type is \"download\".
+      - File state \"unknown\" means the related manager pods on the pod is not running or the node itself is down/disconnected."
+    group: "Longhorn Default Settings"
+    type: int
+    min: 0
+    default: 300
+  - variable: defaultSettings.guaranteedEngineManagerCPU
+    label: Guaranteed Engine Manager CPU
+    description: "This integer value indicates how many percentage of the total allocatable CPU on each node will be reserved for each engine manager Pod. For example, 10 means 10% of the total CPU on a node will be allocated to each engine manager pod on this node. This will help maintain engine stability during high node workload.
+    In order to prevent unexpected volume engine crash as well as guarantee a relative acceptable IO performance, you can use the following formula to calculate a value for this setting:
+    Guaranteed Engine Manager CPU = The estimated max Longhorn volume engine count on a node * 0.1 / The total allocatable CPUs on the node * 100.
+    The result of above calculation doesn't mean that's the maximum CPU resources the Longhorn workloads require. To fully exploit the Longhorn volume I/O performance, you can allocate/guarantee more CPU resources via this setting.
+    If it's hard to estimate the usage now, you can leave it with the default value, which is 12%. Then you can tune it when there is no running workload using Longhorn volumes.
+    WARNING:
+      - Value 0 means unsetting CPU requests for engine manager pods.
+      - Considering the possible new instance manager pods in the further system upgrade, this integer value is range from 0 to 40. And the sum with setting 'Guaranteed Engine Manager CPU' should not be greater than 40.
+      - One more set of instance manager pods may need to be deployed when the Longhorn system is upgraded. If current available CPUs of the nodes are not enough for the new instance manager pods, you need to detach the volumes using the oldest instance manager pods so that Longhorn can clean up the old pods automatically and release the CPU resources. And the new pods with the latest instance manager image will be launched then.
+      - This global setting will be ignored for a node if the field \"EngineManagerCPURequest\" on the node is set.
+      - After this setting is changed, all engine manager pods using this global setting on all the nodes will be automatically restarted. In other words, DO NOT CHANGE THIS SETTING WITH ATTACHED VOLUMES."
+    group: "Longhorn Default Settings"
+    type: int
+    min: 0
+    max: 40
+    default: 12
+  - variable: defaultSettings.guaranteedReplicaManagerCPU
+    label: Guaranteed Replica Manager CPU
+    description: "This integer value indicates how many percentage of the total allocatable CPU on each node will be reserved for each replica manager Pod. 10 means 10% of the total CPU on a node will be allocated to each replica manager pod on this node. This will help maintain replica stability during high node workload.
+    In order to prevent unexpected volume replica crash as well as guarantee a relative acceptable IO performance, you can use the following formula to calculate a value for this setting:
+    Guaranteed Replica Manager CPU = The estimated max Longhorn volume replica count on a node * 0.1 / The total allocatable CPUs on the node * 100.
+    The result of above calculation doesn't mean that's the maximum CPU resources the Longhorn workloads require. To fully exploit the Longhorn volume I/O performance, you can allocate/guarantee more CPU resources via this setting.
+    If it's hard to estimate the usage now, you can leave it with the default value, which is 12%. Then you can tune it when there is no running workload using Longhorn volumes.
+    WARNING:
+      - Value 0 means unsetting CPU requests for replica manager pods.
+      - Considering the possible new instance manager pods in the further system upgrade, this integer value is range from 0 to 40. And the sum with setting 'Guaranteed Replica Manager CPU' should not be greater than 40.
+      - One more set of instance manager pods may need to be deployed when the Longhorn system is upgraded. If current available CPUs of the nodes are not enough for the new instance manager pods, you need to detach the volumes using the oldest instance manager pods so that Longhorn can clean up the old pods automatically and release the CPU resources. And the new pods with the latest instance manager image will be launched then.
+      - This global setting will be ignored for a node if the field \"ReplicaManagerCPURequest\" on the node is set.
+      - After this setting is changed, all replica manager pods using this global setting on all the nodes will be automatically restarted. In other words, DO NOT CHANGE THIS SETTING WITH ATTACHED VOLUMES."
+    group: "Longhorn Default Settings"
+    type: int
+    min: 0
+    max: 40
+    default: 12
+- variable: defaultSettings.kubernetesClusterAutoscalerEnabled
+  label: Kubernetes Cluster Autoscaler Enabled (Experimental)
+  description: "Enabling this setting will notify Longhorn that the cluster is using Kubernetes Cluster Autoscaler.
+  Longhorn prevents data loss by only allowing the Cluster Autoscaler to scale down a node that met all conditions:
+    - No volume attached to the node.
+    - Is not the last node containing the replica of any volume.
+    - Is not running backing image components pod.
+    - Is not running share manager components pod."
+  group: "Longhorn Default Settings"
+  type: boolean
+  default: false
+- variable: defaultSettings.orphanAutoDeletion
+  label: Orphaned Data Cleanup
+  description: "This setting allows Longhorn to delete the orphan resource and its corresponding orphaned data automatically like stale replicas. Orphan resources on down or unknown nodes will not be cleaned up automatically."
+  group: "Longhorn Default Settings"
+  type: boolean
+  default: false
+- variable: defaultSettings.storageNetwork
+  label: Storage Network
+  description: "Longhorn uses the storage network for in-cluster data traffic. Leave this blank to use the Kubernetes cluster network.
+	To segregate the storage network, input the pre-existing NetworkAttachmentDefinition in \"<namespace>/<name>\" format.
+	WARNING:
+	  - The cluster must have pre-existing Multus installed, and NetworkAttachmentDefinition IPs are reachable between nodes.
+	  - DO NOT CHANGE THIS SETTING WITH ATTACHED VOLUMES. Longhorn will try to block this setting update when there are attached volumes.
+	  - When applying the setting, Longhorn will restart all manager, instance-manager, and backing-image-manager pods."
+  group: "Longhorn Default Settings"
+  type: string
+  default:
+- variable: defaultSettings.deletingConfirmationFlag
+  label: Deleting Confirmation Flag
+  description: "This flag is designed to prevent Longhorn from being accidentally uninstalled which will lead to data lost.
+	Set this flag to **true** to allow Longhorn uninstallation.
+	If this flag **false**, Longhorn uninstallation job will fail. "
+  group: "Longhorn Default Settings"
+  type: boolean
+  default: "false"
+- variable: defaultSettings.engineReplicaTimeout
+  label: Timeout between Engine and Replica
+  description: "In seconds. The setting specifies the timeout between the engine and replica(s), and the value should be between 8 to 30 seconds. The default value is 8 seconds."
+  group: "Longhorn Default Settings"
+  type: int
+  default: "8"
+- variable: defaultSettings.snapshotDataIntegrity
+  label: Snapshot Data Integrity
+  description: "This setting allows users to enable or disable snapshot hashing and data integrity checking.
+  Available options are
+    - **disabled**: Disable snapshot disk file hashing and data integrity checking.
+    - **enabled**: Enables periodic snapshot disk file hashing and data integrity checking. To detect the filesystem-unaware corruption caused by bit rot or other issues in snapshot disk files, Longhorn system periodically hashes files and finds corrupted ones. Hence, the system performance will be impacted during the periodical checking.
+    - **fast-check**: Enable snapshot disk file hashing and fast data integrity checking. Longhorn system only hashes snapshot disk files if their are not hashed or the modification time are changed. In this mode, filesystem-unaware corruption cannot be detected, but the impact on system performance can be minimized."
+  group: "Longhorn Default Settings"
+  type: string
+  default: "disabled"
+- variable: defaultSettings.snapshotDataIntegrityImmediateCheckAfterSnapshotCreation
+  label: Immediate Snapshot Data Integrity Check After Creating a Snapshot
+  description: "Hashing snapshot disk files impacts the performance of the system. The immediate snapshot hashing and checking can be disabled to minimize the impact after creating a snapshot."
+  group: "Longhorn Default Settings"
+  type: boolean
+  default: "false"
+- variable: defaultSettings.snapshotDataIntegrityCronjob
+  label: Snapshot Data Integrity Check CronJob
+  description: "Unix-cron string format. The setting specifies when Longhorn checks the data integrity of snapshot disk files.
+  Warning: Hashing snapshot disk files impacts the performance of the system. It is recommended to run data integrity checks during off-peak times and to reduce the frequency of checks."
+  group: "Longhorn Default Settings"
+  type: string
+  default: "0 0 */7 * *"
+- variable: defaultSettings.removeSnapshotsDuringFilesystemTrim
+  label: Remove Snapshots During Filesystem Trim
+  description: "This setting allows Longhorn filesystem trim feature to automatically mark the latest snapshot and its ancestors as removed and stops at the snapshot containing multiple children.\n\n
+    Since Longhorn filesystem trim feature can be applied to the volume head and the followed continuous removed or system snapshots only.\n\n
+    Notice that trying to trim a removed files from a valid snapshot will do nothing but the filesystem will discard this kind of in-memory trimmable file info.\n\n
+    Later on if you mark the snapshot as removed and want to retry the trim, you may need to unmount and remount the filesystem so that the filesystem can recollect the trimmable file info."
+  group: "Longhorn Default Settings"
+  type: boolean
+  default: "false"
+- variable: defaultSettings.fastReplicaRebuildEnabled
+  label: Fast Replica Rebuild Enabled
+  description: "This feature supports the fast replica rebuilding. It relies on the checksum of snapshot disk files, so setting the snapshot-data-integrity to **enable** or **fast-check** is a prerequisite."
+  group: "Longhorn Default Settings"
+  type: boolean
+  default: false
+- variable: defaultSettings.replicaFileSyncHttpClientTimeout
+  label: Timeout of HTTP Client to Replica File Sync Server
+  description: "In seconds. The setting specifies the HTTP client timeout to the file sync server."
+  group: "Longhorn Default Settings"
+  type: int
+  default: "30"
+- variable: persistence.defaultClass
+  default: "true"
+  description: "Set as default StorageClass for Longhorn"
+  label: Default Storage Class
+  group: "Longhorn Storage Class Settings"
+  required: true
+  type: boolean
+- variable: persistence.reclaimPolicy
+  label: Storage Class Retain Policy
+  description: "Define reclaim policy (Retain or Delete)"
+  group: "Longhorn Storage Class Settings"
+  required: true
+  type: enum
+  options:
+  - "Delete"
+  - "Retain"
+  default: "Delete"
+- variable: persistence.defaultClassReplicaCount
+  description: "Set replica count for Longhorn StorageClass"
+  label: Default Storage Class Replica Count
+  group: "Longhorn Storage Class Settings"
+  type: int
+  min: 1
+  max: 10
+  default: 3
+- variable: persistence.defaultDataLocality
+  description: "Set data locality for Longhorn StorageClass"
+  label: Default Storage Class Data Locality
+  group: "Longhorn Storage Class Settings"
+  type: enum
+  options:
+  - "disabled"
+  - "best-effort"
+  default: "disabled"
+- variable: persistence.recurringJobSelector.enable
+  description: "Enable recurring job selector for Longhorn StorageClass"
+  group: "Longhorn Storage Class Settings"
+  label: Enable Storage Class Recurring Job Selector
+  type: boolean
+  default: false
+  show_subquestion_if: true
+  subquestions:
+  - variable: persistence.recurringJobSelector.jobList
+    description: 'Recurring job selector list for Longhorn StorageClass. Please be careful of quotes of input. e.g., [{"name":"backup", "isGroup":true}]'
+    label: Storage Class Recurring Job Selector List
+    group: "Longhorn Storage Class Settings"
+    type: string
+    default:
+- variable: defaultSettings.defaultNodeSelector.enable
+  description: "Enable recurring Node selector for Longhorn StorageClass"
+  group: "Longhorn Storage Class Settings"
+  label: Enable Storage Class Node Selector
+  type: boolean
+  default: false
+  show_subquestion_if: true
+  subquestions:
+  - variable: defaultSettings.defaultNodeSelector.selector
+    label: Storage Class Node Selector
+    description: 'We use NodeSelector when we want to bind PVC via StorageClass into desired mountpoint on the nodes tagged whith its value'
+    group: "Longhorn Default Settings"
+    type: string
+    default:
+- variable: persistence.backingImage.enable
+  description: "Set backing image for Longhorn StorageClass"
+  group: "Longhorn Storage Class Settings"
+  label: Default Storage Class Backing Image
+  type: boolean
+  default: false
+  show_subquestion_if: true
+  subquestions:
+  - variable: persistence.backingImage.name
+    description: 'Specify a backing image that will be used by Longhorn volumes in Longhorn StorageClass. If not exists, the backing image data source type and backing image data source parameters should be specified so that Longhorn will create the backing image before using it.'
+    label: Storage Class Backing Image Name
+    group: "Longhorn Storage Class Settings"
+    type: string
+    default:
+  - variable: persistence.backingImage.expectedChecksum
+    description: 'Specify the expected SHA512 checksum of the selected backing image in Longhorn StorageClass.
+    WARNING:
+      - If the backing image name is not specified, setting this field is meaningless.
+      - It is not recommended to set this field if the data source type is \"export-from-volume\".'
+    label: Storage Class Backing Image Expected SHA512 Checksum
+    group: "Longhorn Storage Class Settings"
+    type: string
+    default:
+  - variable: persistence.backingImage.dataSourceType
+    description: 'Specify the data source type for the backing image used in Longhorn StorageClass.
+    If the backing image does not exists, Longhorn will use this field to create a backing image. Otherwise, Longhorn will use it to verify the selected backing image.
+    WARNING:
+      - If the backing image name is not specified, setting this field is meaningless.
+      - As for backing image creation with data source type \"upload\", it is recommended to do it via UI rather than StorageClass here. Uploading requires file data sending to the Longhorn backend after the object creation, which is complicated if you want to handle it manually.'
+    label: Storage Class Backing Image Data Source Type
+    group: "Longhorn Storage Class Settings"
+    type: enum
+    options:
+    - ""
+    - "download"
+    - "upload"
+    - "export-from-volume"
+    default: ""
+  - variable: persistence.backingImage.dataSourceParameters
+    description: "Specify the data source parameters for the backing image used in Longhorn StorageClass.
+    If the backing image does not exists, Longhorn will use this field to create a backing image. Otherwise, Longhorn will use it to verify the selected backing image.
+    This option accepts a json string of a map. e.g., '{\"url\":\"https://backing-image-example.s3-region.amazonaws.com/test-backing-image\"}'.
+    WARNING:
+      - If the backing image name is not specified, setting this field is meaningless.
+      - Be careful of the quotes here."
+    label: Storage Class Backing Image Data Source Parameters
+    group: "Longhorn Storage Class Settings"
+    type: string
+    default:
+- variable: persistence.removeSnapshotsDuringFilesystemTrim
+  description: "Allow automatically removing snapshots during filesystem trim for Longhorn StorageClass"
+  label: Default Storage Class Remove Snapshots During Filesystem Trim
+  group: "Longhorn Storage Class Settings"
+  type: enum
+  options:
+  - "ignored"
+  - "enabled"
+  - "disabled"
+  default: "ignored"
+- variable: ingress.enabled
+  default: "false"
+  description: "Expose app using Layer 7 Load Balancer - ingress"
+  type: boolean
+  group: "Services and Load Balancing"
+  label: Expose app using Layer 7 Load Balancer
+  show_subquestion_if: true
+  subquestions:
+  - variable: ingress.host
+    default: "xip.io"
+    description: "layer 7 Load Balancer hostname"
+    type: hostname
+    required: true
+    label: Layer 7 Load Balancer Hostname
+  - variable: ingress.path
+    default: "/"
+    description: "If ingress is enabled you can set the default ingress path"
+    type: string
+    required: true
+    label: Ingress Path
+- variable: service.ui.type
+  default: "Rancher-Proxy"
+  description: "Define Longhorn UI service type"
+  type: enum
+  options:
+    - "ClusterIP"
+    - "NodePort"
+    - "LoadBalancer"
+    - "Rancher-Proxy"
+  label: Longhorn UI Service
+  show_if: "ingress.enabled=false"
+  group: "Services and Load Balancing"
+  show_subquestion_if: "NodePort"
+  subquestions:
+  - variable: service.ui.nodePort
+    default: ""
+    description: "NodePort port number(to set explicitly, choose port between 30000-32767)"
+    type: int
+    min: 30000
+    max: 32767
+    show_if: "service.ui.type=NodePort||service.ui.type=LoadBalancer"
+    label: UI Service NodePort number
+- variable: enablePSP
+  default: "false"
+  description: "Setup a pod security policy for Longhorn workloads."
+  label: Pod Security Policy
+  type: boolean
+  group: "Other Settings"
+- variable: global.cattle.windowsCluster.enabled
+  default: "false"
+  description: "Enable this to allow Longhorn to run on the Rancher deployed Windows cluster."
+  label: Rancher Windows Cluster
+  type: boolean
+  group: "Other Settings"
diff --git a/charts/longhorn-1.4.1/templates/NOTES.txt b/charts/longhorn-1.4.1/templates/NOTES.txt
new file mode 100644
index 0000000..cca7cd7
--- /dev/null
+++ b/charts/longhorn-1.4.1/templates/NOTES.txt
@@ -0,0 +1,5 @@
+Longhorn is now installed on the cluster!
+
+Please wait a few minutes for other Longhorn components such as CSI deployments, Engine Images, and Instance Managers to be initialized.
+
+Visit our documentation at https://longhorn.io/docs/
diff --git a/charts/longhorn-1.4.1/templates/_helpers.tpl b/charts/longhorn-1.4.1/templates/_helpers.tpl
new file mode 100644
index 0000000..3fbc2ac
--- /dev/null
+++ b/charts/longhorn-1.4.1/templates/_helpers.tpl
@@ -0,0 +1,66 @@
+{{/* vim: set filetype=mustache: */}}
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "longhorn.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
+{{- 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).
+*/}}
+{{- define "longhorn.fullname" -}}
+{{- $name := default .Chart.Name .Values.nameOverride -}}
+{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
+
+
+{{- define "longhorn.managerIP" -}}
+{{- $fullname := (include "longhorn.fullname" .) -}}
+{{- printf "http://%s-backend:9500" $fullname | trunc 63 | trimSuffix "-" -}}
+{{- end -}}
+
+
+{{- define "secret" }}
+{{- printf "{\"auths\": {\"%s\": {\"auth\": \"%s\"}}}" .Values.privateRegistry.registryUrl (printf "%s:%s" .Values.privateRegistry.registryUser .Values.privateRegistry.registryPasswd | b64enc) | b64enc }}
+{{- end }}
+
+{{- /*
+longhorn.labels generates the standard Helm labels.
+*/ -}}
+{{- define "longhorn.labels" -}}
+app.kubernetes.io/name: {{ template "longhorn.name" . }}
+helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+app.kubernetes.io/version: {{ .Chart.AppVersion }}
+{{- end -}}
+
+
+{{- define "system_default_registry" -}}
+{{- if .Values.global.cattle.systemDefaultRegistry -}}
+{{- printf "%s/" .Values.global.cattle.systemDefaultRegistry -}}
+{{- else -}}
+{{- "" -}}
+{{- end -}}
+{{- end -}}
+
+{{- define "registry_url" -}}
+{{- if .Values.privateRegistry.registryUrl -}}
+{{- printf "%s/" .Values.privateRegistry.registryUrl -}}
+{{- else -}}
+{{ include "system_default_registry" . }}
+{{- end -}}
+{{- end -}}
+
+{{- /*
+ define the longhorn release namespace
+*/ -}}
+{{- define "release_namespace" -}}
+{{- if .Values.namespaceOverride -}}
+{{- .Values.namespaceOverride -}}
+{{- else -}}
+{{- .Release.Namespace -}}
+{{- end -}}
+{{- end -}}
diff --git a/charts/longhorn-1.4.1/templates/clusterrole.yaml b/charts/longhorn-1.4.1/templates/clusterrole.yaml
new file mode 100644
index 0000000..bf28a47
--- /dev/null
+++ b/charts/longhorn-1.4.1/templates/clusterrole.yaml
@@ -0,0 +1,60 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: longhorn-role
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+rules:
+- apiGroups:
+  - apiextensions.k8s.io
+  resources:
+  - customresourcedefinitions
+  verbs:
+  - "*"
+- apiGroups: [""]
+  resources: ["pods", "events", "persistentvolumes", "persistentvolumeclaims","persistentvolumeclaims/status", "nodes", "proxy/nodes", "pods/log", "secrets", "services", "endpoints", "configmaps", "serviceaccounts"]
+  verbs: ["*"]
+- apiGroups: [""]
+  resources: ["namespaces"]
+  verbs: ["get", "list"]
+- apiGroups: ["apps"]
+  resources: ["daemonsets", "statefulsets", "deployments"]
+  verbs: ["*"]
+- apiGroups: ["batch"]
+  resources: ["jobs", "cronjobs"]
+  verbs: ["*"]
+- apiGroups: ["policy"]
+  resources: ["poddisruptionbudgets", "podsecuritypolicies"]
+  verbs: ["*"]
+- apiGroups: ["scheduling.k8s.io"]
+  resources: ["priorityclasses"]
+  verbs: ["watch", "list"]
+- apiGroups: ["storage.k8s.io"]
+  resources: ["storageclasses", "volumeattachments", "volumeattachments/status", "csinodes", "csidrivers"]
+  verbs: ["*"]
+- apiGroups: ["snapshot.storage.k8s.io"]
+  resources: ["volumesnapshotclasses", "volumesnapshots", "volumesnapshotcontents", "volumesnapshotcontents/status"]
+  verbs: ["*"]
+- apiGroups: ["longhorn.io"]
+  resources: ["volumes", "volumes/status", "engines", "engines/status", "replicas", "replicas/status", "settings",
+              "engineimages", "engineimages/status", "nodes", "nodes/status", "instancemanagers", "instancemanagers/status",
+              "sharemanagers", "sharemanagers/status", "backingimages", "backingimages/status",
+              "backingimagemanagers", "backingimagemanagers/status", "backingimagedatasources", "backingimagedatasources/status",
+              "backuptargets", "backuptargets/status", "backupvolumes", "backupvolumes/status", "backups", "backups/status",
+              "recurringjobs", "recurringjobs/status", "orphans", "orphans/status", "snapshots", "snapshots/status",
+              "supportbundles", "supportbundles/status", "systembackups", "systembackups/status", "systemrestores", "systemrestores/status"]
+  verbs: ["*"]
+- apiGroups: ["coordination.k8s.io"]
+  resources: ["leases"]
+  verbs: ["*"]
+- apiGroups: ["metrics.k8s.io"]
+  resources: ["pods", "nodes"]
+  verbs: ["get", "list"]
+- apiGroups: ["apiregistration.k8s.io"]
+  resources: ["apiservices"]
+  verbs: ["list", "watch"]
+- apiGroups: ["admissionregistration.k8s.io"]
+  resources: ["mutatingwebhookconfigurations", "validatingwebhookconfigurations"]
+  verbs: ["get", "list", "create", "patch", "delete"]
+- apiGroups: ["rbac.authorization.k8s.io"]
+  resources: ["roles", "rolebindings", "clusterrolebindings", "clusterroles"]
+  verbs: ["*"]
diff --git a/charts/longhorn-1.4.1/templates/clusterrolebinding.yaml b/charts/longhorn-1.4.1/templates/clusterrolebinding.yaml
new file mode 100644
index 0000000..8ab944b
--- /dev/null
+++ b/charts/longhorn-1.4.1/templates/clusterrolebinding.yaml
@@ -0,0 +1,27 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  name: longhorn-bind
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: longhorn-role
+subjects:
+- kind: ServiceAccount
+  name: longhorn-service-account
+  namespace: {{ include "release_namespace" . }}
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  name: longhorn-support-bundle
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: cluster-admin
+subjects:
+- kind: ServiceAccount
+  name: longhorn-support-bundle
+  namespace: {{ include "release_namespace" . }}
diff --git a/charts/longhorn-1.4.1/templates/crds.yaml b/charts/longhorn-1.4.1/templates/crds.yaml
new file mode 100644
index 0000000..0f73824
--- /dev/null
+++ b/charts/longhorn-1.4.1/templates/crds.yaml
@@ -0,0 +1,3465 @@
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+    longhorn-manager: ""
+  name: backingimagedatasources.longhorn.io
+spec:
+  group: longhorn.io
+  names:
+    kind: BackingImageDataSource
+    listKind: BackingImageDataSourceList
+    plural: backingimagedatasources
+    shortNames:
+    - lhbids
+    singular: backingimagedatasource
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - description: The current state of the pod used to provision the backing image file from source
+      jsonPath: .status.currentState
+      name: State
+      type: string
+    - description: The data source type
+      jsonPath: .spec.sourceType
+      name: SourceType
+      type: string
+    - description: The node the backing image file will be prepared on
+      jsonPath: .spec.nodeID
+      name: Node
+      type: string
+    - description: The disk the backing image file will be prepared on
+      jsonPath: .spec.diskUUID
+      name: DiskUUID
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: BackingImageDataSource is where Longhorn stores backing image data source object.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            x-kubernetes-preserve-unknown-fields: true
+          status:
+            x-kubernetes-preserve-unknown-fields: true
+        type: object
+    served: true
+    storage: false
+    subresources:
+      status: {}
+  - additionalPrinterColumns:
+    - description: The system generated UUID of the provisioned backing image file
+      jsonPath: .spec.uuid
+      name: UUID
+      type: string
+    - description: The current state of the pod used to provision the backing image file from source
+      jsonPath: .status.currentState
+      name: State
+      type: string
+    - description: The data source type
+      jsonPath: .spec.sourceType
+      name: SourceType
+      type: string
+    - description: The backing image file size
+      jsonPath: .status.size
+      name: Size
+      type: string
+    - description: The node the backing image file will be prepared on
+      jsonPath: .spec.nodeID
+      name: Node
+      type: string
+    - description: The disk the backing image file will be prepared on
+      jsonPath: .spec.diskUUID
+      name: DiskUUID
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v1beta2
+    schema:
+      openAPIV3Schema:
+        description: BackingImageDataSource is where Longhorn stores backing image data source object.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: BackingImageDataSourceSpec defines the desired state of the Longhorn backing image data source
+            properties:
+              checksum:
+                type: string
+              diskPath:
+                type: string
+              diskUUID:
+                type: string
+              fileTransferred:
+                type: boolean
+              nodeID:
+                type: string
+              parameters:
+                additionalProperties:
+                  type: string
+                type: object
+              sourceType:
+                enum:
+                - download
+                - upload
+                - export-from-volume
+                type: string
+              uuid:
+                type: string
+            type: object
+          status:
+            description: BackingImageDataSourceStatus defines the observed state of the Longhorn backing image data source
+            properties:
+              checksum:
+                type: string
+              currentState:
+                type: string
+              ip:
+                type: string
+              message:
+                type: string
+              ownerID:
+                type: string
+              progress:
+                type: integer
+              runningParameters:
+                additionalProperties:
+                  type: string
+                nullable: true
+                type: object
+              size:
+                format: int64
+                type: integer
+              storageIP:
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+    longhorn-manager: ""
+  name: backingimagemanagers.longhorn.io
+spec:
+  group: longhorn.io
+  names:
+    kind: BackingImageManager
+    listKind: BackingImageManagerList
+    plural: backingimagemanagers
+    shortNames:
+    - lhbim
+    singular: backingimagemanager
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - description: The current state of the manager
+      jsonPath: .status.currentState
+      name: State
+      type: string
+    - description: The image the manager pod will use
+      jsonPath: .spec.image
+      name: Image
+      type: string
+    - description: The node the manager is on
+      jsonPath: .spec.nodeID
+      name: Node
+      type: string
+    - description: The disk the manager is responsible for
+      jsonPath: .spec.diskUUID
+      name: DiskUUID
+      type: string
+    - description: The disk path the manager is using
+      jsonPath: .spec.diskPath
+      name: DiskPath
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: BackingImageManager is where Longhorn stores backing image manager object.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            x-kubernetes-preserve-unknown-fields: true
+          status:
+            x-kubernetes-preserve-unknown-fields: true
+        type: object
+    served: true
+    storage: false
+    subresources:
+      status: {}
+  - additionalPrinterColumns:
+    - description: The current state of the manager
+      jsonPath: .status.currentState
+      name: State
+      type: string
+    - description: The image the manager pod will use
+      jsonPath: .spec.image
+      name: Image
+      type: string
+    - description: The node the manager is on
+      jsonPath: .spec.nodeID
+      name: Node
+      type: string
+    - description: The disk the manager is responsible for
+      jsonPath: .spec.diskUUID
+      name: DiskUUID
+      type: string
+    - description: The disk path the manager is using
+      jsonPath: .spec.diskPath
+      name: DiskPath
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v1beta2
+    schema:
+      openAPIV3Schema:
+        description: BackingImageManager is where Longhorn stores backing image manager object.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: BackingImageManagerSpec defines the desired state of the Longhorn backing image manager
+            properties:
+              backingImages:
+                additionalProperties:
+                  type: string
+                type: object
+              diskPath:
+                type: string
+              diskUUID:
+                type: string
+              image:
+                type: string
+              nodeID:
+                type: string
+            type: object
+          status:
+            description: BackingImageManagerStatus defines the observed state of the Longhorn backing image manager
+            properties:
+              apiMinVersion:
+                type: integer
+              apiVersion:
+                type: integer
+              backingImageFileMap:
+                additionalProperties:
+                  properties:
+                    currentChecksum:
+                      type: string
+                    directory:
+                      description: 'Deprecated: This field is useless.'
+                      type: string
+                    downloadProgress:
+                      description: 'Deprecated: This field is renamed to `Progress`.'
+                      type: integer
+                    message:
+                      type: string
+                    name:
+                      type: string
+                    progress:
+                      type: integer
+                    senderManagerAddress:
+                      type: string
+                    sendingReference:
+                      type: integer
+                    size:
+                      format: int64
+                      type: integer
+                    state:
+                      type: string
+                    url:
+                      description: 'Deprecated: This field is useless now. The manager of backing image files doesn''t care if a file is downloaded and how.'
+                      type: string
+                    uuid:
+                      type: string
+                  type: object
+                nullable: true
+                type: object
+              currentState:
+                type: string
+              ip:
+                type: string
+              ownerID:
+                type: string
+              storageIP:
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+    longhorn-manager: ""
+  name: backingimages.longhorn.io
+spec:
+  conversion:
+    strategy: Webhook
+    webhook:
+      clientConfig:
+        service:
+          name: longhorn-conversion-webhook
+          namespace: {{ include "release_namespace" . }}
+          path: /v1/webhook/conversion
+          port: 9443
+      conversionReviewVersions:
+      - v1beta2
+      - v1beta1
+  group: longhorn.io
+  names:
+    kind: BackingImage
+    listKind: BackingImageList
+    plural: backingimages
+    shortNames:
+    - lhbi
+    singular: backingimage
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - description: The backing image name
+      jsonPath: .spec.image
+      name: Image
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: BackingImage is where Longhorn stores backing image object.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            x-kubernetes-preserve-unknown-fields: true
+          status:
+            x-kubernetes-preserve-unknown-fields: true
+        type: object
+    served: true
+    storage: false
+    subresources:
+      status: {}
+  - additionalPrinterColumns:
+    - description: The system generated UUID
+      jsonPath: .status.uuid
+      name: UUID
+      type: string
+    - description: The source of the backing image file data
+      jsonPath: .spec.sourceType
+      name: SourceType
+      type: string
+    - description: The backing image file size in each disk
+      jsonPath: .status.size
+      name: Size
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v1beta2
+    schema:
+      openAPIV3Schema:
+        description: BackingImage is where Longhorn stores backing image object.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: BackingImageSpec defines the desired state of the Longhorn backing image
+            properties:
+              checksum:
+                type: string
+              disks:
+                additionalProperties:
+                  type: string
+                type: object
+              imageURL:
+                description: 'Deprecated: This kind of info will be included in the related BackingImageDataSource.'
+                type: string
+              sourceParameters:
+                additionalProperties:
+                  type: string
+                type: object
+              sourceType:
+                enum:
+                - download
+                - upload
+                - export-from-volume
+                type: string
+            type: object
+          status:
+            description: BackingImageStatus defines the observed state of the Longhorn backing image status
+            properties:
+              checksum:
+                type: string
+              diskDownloadProgressMap:
+                additionalProperties:
+                  type: integer
+                description: 'Deprecated: Replaced by field `Progress` in `DiskFileStatusMap`.'
+                nullable: true
+                type: object
+              diskDownloadStateMap:
+                additionalProperties:
+                  description: BackingImageDownloadState is replaced by BackingImageState.
+                  type: string
+                description: 'Deprecated: Replaced by field `State` in `DiskFileStatusMap`.'
+                nullable: true
+                type: object
+              diskFileStatusMap:
+                additionalProperties:
+                  properties:
+                    lastStateTransitionTime:
+                      type: string
+                    message:
+                      type: string
+                    progress:
+                      type: integer
+                    state:
+                      type: string
+                  type: object
+                nullable: true
+                type: object
+              diskLastRefAtMap:
+                additionalProperties:
+                  type: string
+                nullable: true
+                type: object
+              ownerID:
+                type: string
+              size:
+                format: int64
+                type: integer
+              uuid:
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+    longhorn-manager: ""
+  name: backups.longhorn.io
+spec:
+  group: longhorn.io
+  names:
+    kind: Backup
+    listKind: BackupList
+    plural: backups
+    shortNames:
+    - lhb
+    singular: backup
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - description: The snapshot name
+      jsonPath: .status.snapshotName
+      name: SnapshotName
+      type: string
+    - description: The snapshot size
+      jsonPath: .status.size
+      name: SnapshotSize
+      type: string
+    - description: The snapshot creation time
+      jsonPath: .status.snapshotCreatedAt
+      name: SnapshotCreatedAt
+      type: string
+    - description: The backup state
+      jsonPath: .status.state
+      name: State
+      type: string
+    - description: The backup last synced time
+      jsonPath: .status.lastSyncedAt
+      name: LastSyncedAt
+      type: string
+    name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: Backup is where Longhorn stores backup object.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            x-kubernetes-preserve-unknown-fields: true
+          status:
+            x-kubernetes-preserve-unknown-fields: true
+        type: object
+    served: true
+    storage: false
+    subresources:
+      status: {}
+  - additionalPrinterColumns:
+    - description: The snapshot name
+      jsonPath: .status.snapshotName
+      name: SnapshotName
+      type: string
+    - description: The snapshot size
+      jsonPath: .status.size
+      name: SnapshotSize
+      type: string
+    - description: The snapshot creation time
+      jsonPath: .status.snapshotCreatedAt
+      name: SnapshotCreatedAt
+      type: string
+    - description: The backup state
+      jsonPath: .status.state
+      name: State
+      type: string
+    - description: The backup last synced time
+      jsonPath: .status.lastSyncedAt
+      name: LastSyncedAt
+      type: string
+    name: v1beta2
+    schema:
+      openAPIV3Schema:
+        description: Backup is where Longhorn stores backup object.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: BackupSpec defines the desired state of the Longhorn backup
+            properties:
+              labels:
+                additionalProperties:
+                  type: string
+                description: The labels of snapshot backup.
+                type: object
+              snapshotName:
+                description: The snapshot name.
+                type: string
+              syncRequestedAt:
+                description: The time to request run sync the remote backup.
+                format: date-time
+                nullable: true
+                type: string
+            type: object
+          status:
+            description: BackupStatus defines the observed state of the Longhorn backup
+            properties:
+              backupCreatedAt:
+                description: The snapshot backup upload finished time.
+                type: string
+              error:
+                description: The error message when taking the snapshot backup.
+                type: string
+              labels:
+                additionalProperties:
+                  type: string
+                description: The labels of snapshot backup.
+                nullable: true
+                type: object
+              lastSyncedAt:
+                description: The last time that the backup was synced with the remote backup target.
+                format: date-time
+                nullable: true
+                type: string
+              messages:
+                additionalProperties:
+                  type: string
+                description: The error messages when calling longhorn engine on listing or inspecting backups.
+                nullable: true
+                type: object
+              ownerID:
+                description: The node ID on which the controller is responsible to reconcile this backup CR.
+                type: string
+              progress:
+                description: The snapshot backup progress.
+                type: integer
+              replicaAddress:
+                description: The address of the replica that runs snapshot backup.
+                type: string
+              size:
+                description: The snapshot size.
+                type: string
+              snapshotCreatedAt:
+                description: The snapshot creation time.
+                type: string
+              snapshotName:
+                description: The snapshot name.
+                type: string
+              state:
+                description: The backup creation state. Can be "", "InProgress", "Completed", "Error", "Unknown".
+                type: string
+              url:
+                description: The snapshot backup URL.
+                type: string
+              volumeBackingImageName:
+                description: The volume's backing image name.
+                type: string
+              volumeCreated:
+                description: The volume creation time.
+                type: string
+              volumeName:
+                description: The volume name.
+                type: string
+              volumeSize:
+                description: The volume size.
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+    longhorn-manager: ""
+  name: backuptargets.longhorn.io
+spec:
+  conversion:
+    strategy: Webhook
+    webhook:
+      clientConfig:
+        service:
+          name: longhorn-conversion-webhook
+          namespace: {{ include "release_namespace" . }}
+          path: /v1/webhook/conversion
+          port: 9443
+      conversionReviewVersions:
+      - v1beta2
+      - v1beta1
+  group: longhorn.io
+  names:
+    kind: BackupTarget
+    listKind: BackupTargetList
+    plural: backuptargets
+    shortNames:
+    - lhbt
+    singular: backuptarget
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - description: The backup target URL
+      jsonPath: .spec.backupTargetURL
+      name: URL
+      type: string
+    - description: The backup target credential secret
+      jsonPath: .spec.credentialSecret
+      name: Credential
+      type: string
+    - description: The backup target poll interval
+      jsonPath: .spec.pollInterval
+      name: LastBackupAt
+      type: string
+    - description: Indicate whether the backup target is available or not
+      jsonPath: .status.available
+      name: Available
+      type: boolean
+    - description: The backup target last synced time
+      jsonPath: .status.lastSyncedAt
+      name: LastSyncedAt
+      type: string
+    name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: BackupTarget is where Longhorn stores backup target object.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            x-kubernetes-preserve-unknown-fields: true
+          status:
+            x-kubernetes-preserve-unknown-fields: true
+        type: object
+    served: true
+    storage: false
+    subresources:
+      status: {}
+  - additionalPrinterColumns:
+    - description: The backup target URL
+      jsonPath: .spec.backupTargetURL
+      name: URL
+      type: string
+    - description: The backup target credential secret
+      jsonPath: .spec.credentialSecret
+      name: Credential
+      type: string
+    - description: The backup target poll interval
+      jsonPath: .spec.pollInterval
+      name: LastBackupAt
+      type: string
+    - description: Indicate whether the backup target is available or not
+      jsonPath: .status.available
+      name: Available
+      type: boolean
+    - description: The backup target last synced time
+      jsonPath: .status.lastSyncedAt
+      name: LastSyncedAt
+      type: string
+    name: v1beta2
+    schema:
+      openAPIV3Schema:
+        description: BackupTarget is where Longhorn stores backup target object.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: BackupTargetSpec defines the desired state of the Longhorn backup target
+            properties:
+              backupTargetURL:
+                description: The backup target URL.
+                type: string
+              credentialSecret:
+                description: The backup target credential secret.
+                type: string
+              pollInterval:
+                description: The interval that the cluster needs to run sync with the backup target.
+                type: string
+              syncRequestedAt:
+                description: The time to request run sync the remote backup target.
+                format: date-time
+                nullable: true
+                type: string
+            type: object
+          status:
+            description: BackupTargetStatus defines the observed state of the Longhorn backup target
+            properties:
+              available:
+                description: Available indicates if the remote backup target is available or not.
+                type: boolean
+              conditions:
+                description: Records the reason on why the backup target is unavailable.
+                items:
+                  properties:
+                    lastProbeTime:
+                      description: Last time we probed the condition.
+                      type: string
+                    lastTransitionTime:
+                      description: Last time the condition transitioned from one status to another.
+                      type: string
+                    message:
+                      description: Human-readable message indicating details about last transition.
+                      type: string
+                    reason:
+                      description: Unique, one-word, CamelCase reason for the condition's last transition.
+                      type: string
+                    status:
+                      description: Status is the status of the condition. Can be True, False, Unknown.
+                      type: string
+                    type:
+                      description: Type is the type of the condition.
+                      type: string
+                  type: object
+                nullable: true
+                type: array
+              lastSyncedAt:
+                description: The last time that the controller synced with the remote backup target.
+                format: date-time
+                nullable: true
+                type: string
+              ownerID:
+                description: The node ID on which the controller is responsible to reconcile this backup target CR.
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+    longhorn-manager: ""
+  name: backupvolumes.longhorn.io
+spec:
+  group: longhorn.io
+  names:
+    kind: BackupVolume
+    listKind: BackupVolumeList
+    plural: backupvolumes
+    shortNames:
+    - lhbv
+    singular: backupvolume
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - description: The backup volume creation time
+      jsonPath: .status.createdAt
+      name: CreatedAt
+      type: string
+    - description: The backup volume last backup name
+      jsonPath: .status.lastBackupName
+      name: LastBackupName
+      type: string
+    - description: The backup volume last backup time
+      jsonPath: .status.lastBackupAt
+      name: LastBackupAt
+      type: string
+    - description: The backup volume last synced time
+      jsonPath: .status.lastSyncedAt
+      name: LastSyncedAt
+      type: string
+    name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: BackupVolume is where Longhorn stores backup volume object.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            x-kubernetes-preserve-unknown-fields: true
+          status:
+            x-kubernetes-preserve-unknown-fields: true
+        type: object
+    served: true
+    storage: false
+    subresources:
+      status: {}
+  - additionalPrinterColumns:
+    - description: The backup volume creation time
+      jsonPath: .status.createdAt
+      name: CreatedAt
+      type: string
+    - description: The backup volume last backup name
+      jsonPath: .status.lastBackupName
+      name: LastBackupName
+      type: string
+    - description: The backup volume last backup time
+      jsonPath: .status.lastBackupAt
+      name: LastBackupAt
+      type: string
+    - description: The backup volume last synced time
+      jsonPath: .status.lastSyncedAt
+      name: LastSyncedAt
+      type: string
+    name: v1beta2
+    schema:
+      openAPIV3Schema:
+        description: BackupVolume is where Longhorn stores backup volume object.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: BackupVolumeSpec defines the desired state of the Longhorn backup volume
+            properties:
+              syncRequestedAt:
+                description: The time to request run sync the remote backup volume.
+                format: date-time
+                nullable: true
+                type: string
+            type: object
+          status:
+            description: BackupVolumeStatus defines the observed state of the Longhorn backup volume
+            properties:
+              backingImageChecksum:
+                description: the backing image checksum.
+                type: string
+              backingImageName:
+                description: The backing image name.
+                type: string
+              createdAt:
+                description: The backup volume creation time.
+                type: string
+              dataStored:
+                description: The backup volume block count.
+                type: string
+              labels:
+                additionalProperties:
+                  type: string
+                description: The backup volume labels.
+                nullable: true
+                type: object
+              lastBackupAt:
+                description: The latest volume backup time.
+                type: string
+              lastBackupName:
+                description: The latest volume backup name.
+                type: string
+              lastModificationTime:
+                description: The backup volume config last modification time.
+                format: date-time
+                nullable: true
+                type: string
+              lastSyncedAt:
+                description: The last time that the backup volume was synced into the cluster.
+                format: date-time
+                nullable: true
+                type: string
+              messages:
+                additionalProperties:
+                  type: string
+                description: The error messages when call longhorn engine on list or inspect backup volumes.
+                nullable: true
+                type: object
+              ownerID:
+                description: The node ID on which the controller is responsible to reconcile this backup volume CR.
+                type: string
+              size:
+                description: The backup volume size.
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+    longhorn-manager: ""
+  name: engineimages.longhorn.io
+spec:
+  preserveUnknownFields: false
+  conversion:
+    strategy: Webhook
+    webhook:
+      clientConfig:
+        service:
+          name: longhorn-conversion-webhook
+          namespace: {{ include "release_namespace" . }}
+          path: /v1/webhook/conversion
+          port: 9443
+      conversionReviewVersions:
+      - v1beta2
+      - v1beta1
+  group: longhorn.io
+  names:
+    kind: EngineImage
+    listKind: EngineImageList
+    plural: engineimages
+    shortNames:
+    - lhei
+    singular: engineimage
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - description: State of the engine image
+      jsonPath: .status.state
+      name: State
+      type: string
+    - description: The Longhorn engine image
+      jsonPath: .spec.image
+      name: Image
+      type: string
+    - description: Number of resources using the engine image
+      jsonPath: .status.refCount
+      name: RefCount
+      type: integer
+    - description: The build date of the engine image
+      jsonPath: .status.buildDate
+      name: BuildDate
+      type: date
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: EngineImage is where Longhorn stores engine image object.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            x-kubernetes-preserve-unknown-fields: true
+          status:
+            x-kubernetes-preserve-unknown-fields: true
+        type: object
+    served: true
+    storage: false
+    subresources:
+      status: {}
+  - additionalPrinterColumns:
+    - description: State of the engine image
+      jsonPath: .status.state
+      name: State
+      type: string
+    - description: The Longhorn engine image
+      jsonPath: .spec.image
+      name: Image
+      type: string
+    - description: Number of resources using the engine image
+      jsonPath: .status.refCount
+      name: RefCount
+      type: integer
+    - description: The build date of the engine image
+      jsonPath: .status.buildDate
+      name: BuildDate
+      type: date
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v1beta2
+    schema:
+      openAPIV3Schema:
+        description: EngineImage is where Longhorn stores engine image object.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: EngineImageSpec defines the desired state of the Longhorn engine image
+            properties:
+              image:
+                minLength: 1
+                type: string
+            required:
+            - image
+            type: object
+          status:
+            description: EngineImageStatus defines the observed state of the Longhorn engine image
+            properties:
+              buildDate:
+                type: string
+              cliAPIMinVersion:
+                type: integer
+              cliAPIVersion:
+                type: integer
+              conditions:
+                items:
+                  properties:
+                    lastProbeTime:
+                      description: Last time we probed the condition.
+                      type: string
+                    lastTransitionTime:
+                      description: Last time the condition transitioned from one status to another.
+                      type: string
+                    message:
+                      description: Human-readable message indicating details about last transition.
+                      type: string
+                    reason:
+                      description: Unique, one-word, CamelCase reason for the condition's last transition.
+                      type: string
+                    status:
+                      description: Status is the status of the condition. Can be True, False, Unknown.
+                      type: string
+                    type:
+                      description: Type is the type of the condition.
+                      type: string
+                  type: object
+                nullable: true
+                type: array
+              controllerAPIMinVersion:
+                type: integer
+              controllerAPIVersion:
+                type: integer
+              dataFormatMinVersion:
+                type: integer
+              dataFormatVersion:
+                type: integer
+              gitCommit:
+                type: string
+              noRefSince:
+                type: string
+              nodeDeploymentMap:
+                additionalProperties:
+                  type: boolean
+                nullable: true
+                type: object
+              ownerID:
+                type: string
+              refCount:
+                type: integer
+              state:
+                type: string
+              version:
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+    longhorn-manager: ""
+  name: engines.longhorn.io
+spec:
+  group: longhorn.io
+  names:
+    kind: Engine
+    listKind: EngineList
+    plural: engines
+    shortNames:
+    - lhe
+    singular: engine
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - description: The current state of the engine
+      jsonPath: .status.currentState
+      name: State
+      type: string
+    - description: The node that the engine is on
+      jsonPath: .spec.nodeID
+      name: Node
+      type: string
+    - description: The instance manager of the engine
+      jsonPath: .status.instanceManagerName
+      name: InstanceManager
+      type: string
+    - description: The current image of the engine
+      jsonPath: .status.currentImage
+      name: Image
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: Engine is where Longhorn stores engine object.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            x-kubernetes-preserve-unknown-fields: true
+          status:
+            x-kubernetes-preserve-unknown-fields: true
+        type: object
+    served: true
+    storage: false
+    subresources:
+      status: {}
+  - additionalPrinterColumns:
+    - description: The current state of the engine
+      jsonPath: .status.currentState
+      name: State
+      type: string
+    - description: The node that the engine is on
+      jsonPath: .spec.nodeID
+      name: Node
+      type: string
+    - description: The instance manager of the engine
+      jsonPath: .status.instanceManagerName
+      name: InstanceManager
+      type: string
+    - description: The current image of the engine
+      jsonPath: .status.currentImage
+      name: Image
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v1beta2
+    schema:
+      openAPIV3Schema:
+        description: Engine is where Longhorn stores engine object.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: EngineSpec defines the desired state of the Longhorn engine
+            properties:
+              active:
+                type: boolean
+              backupVolume:
+                type: string
+              desireState:
+                type: string
+              disableFrontend:
+                type: boolean
+              engineImage:
+                type: string
+              frontend:
+                enum:
+                - blockdev
+                - iscsi
+                - ""
+                type: string
+              logRequested:
+                type: boolean
+              nodeID:
+                type: string
+              replicaAddressMap:
+                additionalProperties:
+                  type: string
+                type: object
+              requestedBackupRestore:
+                type: string
+              requestedDataSource:
+                type: string
+              revisionCounterDisabled:
+                type: boolean
+              salvageRequested:
+                type: boolean
+              unmapMarkSnapChainRemovedEnabled:
+                type: boolean
+              upgradedReplicaAddressMap:
+                additionalProperties:
+                  type: string
+                type: object
+              volumeName:
+                type: string
+              volumeSize:
+                format: int64
+                type: string
+            type: object
+          status:
+            description: EngineStatus defines the observed state of the Longhorn engine
+            properties:
+              backupStatus:
+                additionalProperties:
+                  properties:
+                    backupURL:
+                      type: string
+                    error:
+                      type: string
+                    progress:
+                      type: integer
+                    replicaAddress:
+                      type: string
+                    snapshotName:
+                      type: string
+                    state:
+                      type: string
+                  type: object
+                nullable: true
+                type: object
+              cloneStatus:
+                additionalProperties:
+                  properties:
+                    error:
+                      type: string
+                    fromReplicaAddress:
+                      type: string
+                    isCloning:
+                      type: boolean
+                    progress:
+                      type: integer
+                    snapshotName:
+                      type: string
+                    state:
+                      type: string
+                  type: object
+                nullable: true
+                type: object
+              conditions:
+                items:
+                  properties:
+                    lastProbeTime:
+                      description: Last time we probed the condition.
+                      type: string
+                    lastTransitionTime:
+                      description: Last time the condition transitioned from one status to another.
+                      type: string
+                    message:
+                      description: Human-readable message indicating details about last transition.
+                      type: string
+                    reason:
+                      description: Unique, one-word, CamelCase reason for the condition's last transition.
+                      type: string
+                    status:
+                      description: Status is the status of the condition. Can be True, False, Unknown.
+                      type: string
+                    type:
+                      description: Type is the type of the condition.
+                      type: string
+                  type: object
+                nullable: true
+                type: array
+              currentImage:
+                type: string
+              currentReplicaAddressMap:
+                additionalProperties:
+                  type: string
+                nullable: true
+                type: object
+              currentSize:
+                format: int64
+                type: string
+              currentState:
+                type: string
+              endpoint:
+                type: string
+              instanceManagerName:
+                type: string
+              ip:
+                type: string
+              isExpanding:
+                type: boolean
+              lastExpansionError:
+                type: string
+              lastExpansionFailedAt:
+                type: string
+              lastRestoredBackup:
+                type: string
+              logFetched:
+                type: boolean
+              ownerID:
+                type: string
+              port:
+                type: integer
+              purgeStatus:
+                additionalProperties:
+                  properties:
+                    error:
+                      type: string
+                    isPurging:
+                      type: boolean
+                    progress:
+                      type: integer
+                    state:
+                      type: string
+                  type: object
+                nullable: true
+                type: object
+              rebuildStatus:
+                additionalProperties:
+                  properties:
+                    error:
+                      type: string
+                    fromReplicaAddress:
+                      type: string
+                    isRebuilding:
+                      type: boolean
+                    progress:
+                      type: integer
+                    state:
+                      type: string
+                  type: object
+                nullable: true
+                type: object
+              replicaModeMap:
+                additionalProperties:
+                  type: string
+                nullable: true
+                type: object
+              restoreStatus:
+                additionalProperties:
+                  properties:
+                    backupURL:
+                      type: string
+                    currentRestoringBackup:
+                      type: string
+                    error:
+                      type: string
+                    filename:
+                      type: string
+                    isRestoring:
+                      type: boolean
+                    lastRestored:
+                      type: string
+                    progress:
+                      type: integer
+                    state:
+                      type: string
+                  type: object
+                nullable: true
+                type: object
+              salvageExecuted:
+                type: boolean
+              snapshots:
+                additionalProperties:
+                  properties:
+                    children:
+                      additionalProperties:
+                        type: boolean
+                      nullable: true
+                      type: object
+                    created:
+                      type: string
+                    labels:
+                      additionalProperties:
+                        type: string
+                      nullable: true
+                      type: object
+                    name:
+                      type: string
+                    parent:
+                      type: string
+                    removed:
+                      type: boolean
+                    size:
+                      type: string
+                    usercreated:
+                      type: boolean
+                  type: object
+                nullable: true
+                type: object
+              snapshotsError:
+                type: string
+              started:
+                type: boolean
+              storageIP:
+                type: string
+              unmapMarkSnapChainRemovedEnabled:
+                type: boolean
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+    longhorn-manager: ""
+  name: instancemanagers.longhorn.io
+spec:
+  group: longhorn.io
+  names:
+    kind: InstanceManager
+    listKind: InstanceManagerList
+    plural: instancemanagers
+    shortNames:
+    - lhim
+    singular: instancemanager
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - description: The state of the instance manager
+      jsonPath: .status.currentState
+      name: State
+      type: string
+    - description: The type of the instance manager (engine or replica)
+      jsonPath: .spec.type
+      name: Type
+      type: string
+    - description: The node that the instance manager is running on
+      jsonPath: .spec.nodeID
+      name: Node
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: InstanceManager is where Longhorn stores instance manager object.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            x-kubernetes-preserve-unknown-fields: true
+          status:
+            x-kubernetes-preserve-unknown-fields: true
+        type: object
+    served: true
+    storage: false
+    subresources:
+      status: {}
+  - additionalPrinterColumns:
+    - description: The state of the instance manager
+      jsonPath: .status.currentState
+      name: State
+      type: string
+    - description: The type of the instance manager (engine or replica)
+      jsonPath: .spec.type
+      name: Type
+      type: string
+    - description: The node that the instance manager is running on
+      jsonPath: .spec.nodeID
+      name: Node
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v1beta2
+    schema:
+      openAPIV3Schema:
+        description: InstanceManager is where Longhorn stores instance manager object.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: InstanceManagerSpec defines the desired state of the Longhorn instancer manager
+            properties:
+              engineImage:
+                description: 'Deprecated: This field is useless.'
+                type: string
+              image:
+                type: string
+              nodeID:
+                type: string
+              type:
+                enum:
+                - engine
+                - replica
+                type: string
+            type: object
+          status:
+            description: InstanceManagerStatus defines the observed state of the Longhorn instance manager
+            properties:
+              apiMinVersion:
+                type: integer
+              apiVersion:
+                type: integer
+              proxyApiMinVersion:
+                type: integer
+              proxyApiVersion:
+                type: integer
+              currentState:
+                type: string
+              instances:
+                additionalProperties:
+                  properties:
+                    spec:
+                      properties:
+                        name:
+                          type: string
+                      type: object
+                    status:
+                      properties:
+                        endpoint:
+                          type: string
+                        errorMsg:
+                          type: string
+                        listen:
+                          type: string
+                        portEnd:
+                          format: int32
+                          type: integer
+                        portStart:
+                          format: int32
+                          type: integer
+                        resourceVersion:
+                          format: int64
+                          type: integer
+                        state:
+                          type: string
+                        type:
+                          type: string
+                      type: object
+                  type: object
+                nullable: true
+                type: object
+              ip:
+                type: string
+              ownerID:
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+    longhorn-manager: ""
+  name: nodes.longhorn.io
+spec:
+  preserveUnknownFields: false
+  conversion:
+    strategy: Webhook
+    webhook:
+      clientConfig:
+        service:
+          name: longhorn-conversion-webhook
+          namespace: {{ include "release_namespace" . }}
+          path: /v1/webhook/conversion
+          port: 9443
+      conversionReviewVersions:
+      - v1beta2
+      - v1beta1
+  group: longhorn.io
+  names:
+    kind: Node
+    listKind: NodeList
+    plural: nodes
+    shortNames:
+    - lhn
+    singular: node
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - description: Indicate whether the node is ready
+      jsonPath: .status.conditions['Ready']['status']
+      name: Ready
+      type: string
+    - description: Indicate whether the user disabled/enabled replica scheduling for the node
+      jsonPath: .spec.allowScheduling
+      name: AllowScheduling
+      type: boolean
+    - description: Indicate whether Longhorn can schedule replicas on the node
+      jsonPath: .status.conditions['Schedulable']['status']
+      name: Schedulable
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: Node is where Longhorn stores Longhorn node object.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            x-kubernetes-preserve-unknown-fields: true
+          status:
+            x-kubernetes-preserve-unknown-fields: true
+        type: object
+    served: true
+    storage: false
+    subresources:
+      status: {}
+  - additionalPrinterColumns:
+    - description: Indicate whether the node is ready
+      jsonPath: .status.conditions[?(@.type=='Ready')].status
+      name: Ready
+      type: string
+    - description: Indicate whether the user disabled/enabled replica scheduling for the node
+      jsonPath: .spec.allowScheduling
+      name: AllowScheduling
+      type: boolean
+    - description: Indicate whether Longhorn can schedule replicas on the node
+      jsonPath: .status.conditions[?(@.type=='Schedulable')].status
+      name: Schedulable
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v1beta2
+    schema:
+      openAPIV3Schema:
+        description: Node is where Longhorn stores Longhorn node object.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: NodeSpec defines the desired state of the Longhorn node
+            properties:
+              allowScheduling:
+                type: boolean
+              disks:
+                additionalProperties:
+                  properties:
+                    allowScheduling:
+                      type: boolean
+                    evictionRequested:
+                      type: boolean
+                    path:
+                      type: string
+                    storageReserved:
+                      format: int64
+                      type: integer
+                    tags:
+                      items:
+                        type: string
+                      type: array
+                  type: object
+                type: object
+              engineManagerCPURequest:
+                type: integer
+              evictionRequested:
+                type: boolean
+              name:
+                type: string
+              replicaManagerCPURequest:
+                type: integer
+              tags:
+                items:
+                  type: string
+                type: array
+            type: object
+          status:
+            description: NodeStatus defines the observed state of the Longhorn node
+            properties:
+              conditions:
+                items:
+                  properties:
+                    lastProbeTime:
+                      description: Last time we probed the condition.
+                      type: string
+                    lastTransitionTime:
+                      description: Last time the condition transitioned from one status to another.
+                      type: string
+                    message:
+                      description: Human-readable message indicating details about last transition.
+                      type: string
+                    reason:
+                      description: Unique, one-word, CamelCase reason for the condition's last transition.
+                      type: string
+                    status:
+                      description: Status is the status of the condition. Can be True, False, Unknown.
+                      type: string
+                    type:
+                      description: Type is the type of the condition.
+                      type: string
+                  type: object
+                nullable: true
+                type: array
+              diskStatus:
+                additionalProperties:
+                  properties:
+                    conditions:
+                      items:
+                        properties:
+                          lastProbeTime:
+                            description: Last time we probed the condition.
+                            type: string
+                          lastTransitionTime:
+                            description: Last time the condition transitioned from one status to another.
+                            type: string
+                          message:
+                            description: Human-readable message indicating details about last transition.
+                            type: string
+                          reason:
+                            description: Unique, one-word, CamelCase reason for the condition's last transition.
+                            type: string
+                          status:
+                            description: Status is the status of the condition. Can be True, False, Unknown.
+                            type: string
+                          type:
+                            description: Type is the type of the condition.
+                            type: string
+                        type: object
+                      nullable: true
+                      type: array
+                    diskUUID:
+                      type: string
+                    scheduledReplica:
+                      additionalProperties:
+                        format: int64
+                        type: integer
+                      nullable: true
+                      type: object
+                    storageAvailable:
+                      format: int64
+                      type: integer
+                    storageMaximum:
+                      format: int64
+                      type: integer
+                    storageScheduled:
+                      format: int64
+                      type: integer
+                  type: object
+                nullable: true
+                type: object
+              region:
+                type: string
+              snapshotCheckStatus:
+                properties:
+                  lastPeriodicCheckedAt:
+                    format: date-time
+                    type: string
+                  snapshotCheckState:
+                    type: string
+                type: object
+              zone:
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+    longhorn-manager: ""
+  name: orphans.longhorn.io
+spec:
+  group: longhorn.io
+  names:
+    kind: Orphan
+    listKind: OrphanList
+    plural: orphans
+    shortNames:
+    - lho
+    singular: orphan
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - description: The type of the orphan
+      jsonPath: .spec.orphanType
+      name: Type
+      type: string
+    - description: The node that the orphan is on
+      jsonPath: .spec.nodeID
+      name: Node
+      type: string
+    name: v1beta2
+    schema:
+      openAPIV3Schema:
+        description: Orphan is where Longhorn stores orphan object.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: OrphanSpec defines the desired state of the Longhorn orphaned data
+            properties:
+              nodeID:
+                description: The node ID on which the controller is responsible to reconcile this orphan CR.
+                type: string
+              orphanType:
+                description: The type of the orphaned data. Can be "replica".
+                type: string
+              parameters:
+                additionalProperties:
+                  type: string
+                description: The parameters of the orphaned data
+                type: object
+            type: object
+          status:
+            description: OrphanStatus defines the observed state of the Longhorn orphaned data
+            properties:
+              conditions:
+                items:
+                  properties:
+                    lastProbeTime:
+                      description: Last time we probed the condition.
+                      type: string
+                    lastTransitionTime:
+                      description: Last time the condition transitioned from one status to another.
+                      type: string
+                    message:
+                      description: Human-readable message indicating details about last transition.
+                      type: string
+                    reason:
+                      description: Unique, one-word, CamelCase reason for the condition's last transition.
+                      type: string
+                    status:
+                      description: Status is the status of the condition. Can be True, False, Unknown.
+                      type: string
+                    type:
+                      description: Type is the type of the condition.
+                      type: string
+                  type: object
+                nullable: true
+                type: array
+              ownerID:
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  labels:
+    longhorn-manager: ""
+  name: recurringjobs.longhorn.io
+spec:
+  group: longhorn.io
+  names:
+    kind: RecurringJob
+    listKind: RecurringJobList
+    plural: recurringjobs
+    shortNames:
+    - lhrj
+    singular: recurringjob
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - description: Sets groupings to the jobs. When set to "default" group will be added to the volume label when no other job label exist in volume
+      jsonPath: .spec.groups
+      name: Groups
+      type: string
+    - description: Should be one of "backup" or "snapshot"
+      jsonPath: .spec.task
+      name: Task
+      type: string
+    - description: The cron expression represents recurring job scheduling
+      jsonPath: .spec.cron
+      name: Cron
+      type: string
+    - description: The number of snapshots/backups to keep for the volume
+      jsonPath: .spec.retain
+      name: Retain
+      type: integer
+    - description: The concurrent job to run by each cron job
+      jsonPath: .spec.concurrency
+      name: Concurrency
+      type: integer
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    - description: Specify the labels
+      jsonPath: .spec.labels
+      name: Labels
+      type: string
+    name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: RecurringJob is where Longhorn stores recurring job object.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            x-kubernetes-preserve-unknown-fields: true
+          status:
+            x-kubernetes-preserve-unknown-fields: true
+        type: object
+    served: true
+    storage: false
+    subresources:
+      status: {}
+  - additionalPrinterColumns:
+    - description: Sets groupings to the jobs. When set to "default" group will be added to the volume label when no other job label exist in volume
+      jsonPath: .spec.groups
+      name: Groups
+      type: string
+    - description: Should be one of "snapshot", "snapshot-cleanup", "snapshot-delete" or "backup"
+      jsonPath: .spec.task
+      name: Task
+      type: string
+    - description: The cron expression represents recurring job scheduling
+      jsonPath: .spec.cron
+      name: Cron
+      type: string
+    - description: The number of snapshots/backups to keep for the volume
+      jsonPath: .spec.retain
+      name: Retain
+      type: integer
+    - description: The concurrent job to run by each cron job
+      jsonPath: .spec.concurrency
+      name: Concurrency
+      type: integer
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    - description: Specify the labels
+      jsonPath: .spec.labels
+      name: Labels
+      type: string
+    name: v1beta2
+    schema:
+      openAPIV3Schema:
+        description: RecurringJob is where Longhorn stores recurring job object.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: RecurringJobSpec defines the desired state of the Longhorn recurring job
+            properties:
+              concurrency:
+                description: The concurrency of taking the snapshot/backup.
+                type: integer
+              cron:
+                description: The cron setting.
+                type: string
+              groups:
+                description: The recurring job group.
+                items:
+                  type: string
+                type: array
+              labels:
+                additionalProperties:
+                  type: string
+                description: The label of the snapshot/backup.
+                type: object
+              name:
+                description: The recurring job name.
+                type: string
+              retain:
+                description: The retain count of the snapshot/backup.
+                type: integer
+              task:
+                description: The recurring job task. Can be "snapshot", "snapshot-cleanup", "snapshot-delete" or "backup".
+                enum:
+                - snapshot
+                - snapshot-cleanup
+                - snapshot-delete
+                - backup
+                type: string
+            type: object
+          status:
+            description: RecurringJobStatus defines the observed state of the Longhorn recurring job
+            properties:
+              ownerID:
+                description: The owner ID which is responsible to reconcile this recurring job CR.
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+    longhorn-manager: ""
+  name: replicas.longhorn.io
+spec:
+  group: longhorn.io
+  names:
+    kind: Replica
+    listKind: ReplicaList
+    plural: replicas
+    shortNames:
+    - lhr
+    singular: replica
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - description: The current state of the replica
+      jsonPath: .status.currentState
+      name: State
+      type: string
+    - description: The node that the replica is on
+      jsonPath: .spec.nodeID
+      name: Node
+      type: string
+    - description: The disk that the replica is on
+      jsonPath: .spec.diskID
+      name: Disk
+      type: string
+    - description: The instance manager of the replica
+      jsonPath: .status.instanceManagerName
+      name: InstanceManager
+      type: string
+    - description: The current image of the replica
+      jsonPath: .status.currentImage
+      name: Image
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: Replica is where Longhorn stores replica object.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            x-kubernetes-preserve-unknown-fields: true
+          status:
+            x-kubernetes-preserve-unknown-fields: true
+        type: object
+    served: true
+    storage: false
+    subresources:
+      status: {}
+  - additionalPrinterColumns:
+    - description: The current state of the replica
+      jsonPath: .status.currentState
+      name: State
+      type: string
+    - description: The node that the replica is on
+      jsonPath: .spec.nodeID
+      name: Node
+      type: string
+    - description: The disk that the replica is on
+      jsonPath: .spec.diskID
+      name: Disk
+      type: string
+    - description: The instance manager of the replica
+      jsonPath: .status.instanceManagerName
+      name: InstanceManager
+      type: string
+    - description: The current image of the replica
+      jsonPath: .status.currentImage
+      name: Image
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v1beta2
+    schema:
+      openAPIV3Schema:
+        description: Replica is where Longhorn stores replica object.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: ReplicaSpec defines the desired state of the Longhorn replica
+            properties:
+              active:
+                type: boolean
+              backingImage:
+                type: string
+              baseImage:
+                description: Deprecated. Rename to BackingImage
+                type: string
+              dataDirectoryName:
+                type: string
+              dataPath:
+                description: Deprecated
+                type: string
+              desireState:
+                type: string
+              diskID:
+                type: string
+              diskPath:
+                type: string
+              engineImage:
+                type: string
+              engineName:
+                type: string
+              failedAt:
+                type: string
+              hardNodeAffinity:
+                type: string
+              healthyAt:
+                type: string
+              logRequested:
+                type: boolean
+              nodeID:
+                type: string
+              rebuildRetryCount:
+                type: integer
+              revisionCounterDisabled:
+                type: boolean
+              salvageRequested:
+                type: boolean
+              unmapMarkDiskChainRemovedEnabled:
+                type: boolean
+              volumeName:
+                type: string
+              volumeSize:
+                format: int64
+                type: string
+            type: object
+          status:
+            description: ReplicaStatus defines the observed state of the Longhorn replica
+            properties:
+              conditions:
+                items:
+                  properties:
+                    lastProbeTime:
+                      description: Last time we probed the condition.
+                      type: string
+                    lastTransitionTime:
+                      description: Last time the condition transitioned from one status to another.
+                      type: string
+                    message:
+                      description: Human-readable message indicating details about last transition.
+                      type: string
+                    reason:
+                      description: Unique, one-word, CamelCase reason for the condition's last transition.
+                      type: string
+                    status:
+                      description: Status is the status of the condition. Can be True, False, Unknown.
+                      type: string
+                    type:
+                      description: Type is the type of the condition.
+                      type: string
+                  type: object
+                nullable: true
+                type: array
+              currentImage:
+                type: string
+              currentState:
+                type: string
+              evictionRequested:
+                type: boolean
+              instanceManagerName:
+                type: string
+              ip:
+                type: string
+              logFetched:
+                type: boolean
+              ownerID:
+                type: string
+              port:
+                type: integer
+              salvageExecuted:
+                type: boolean
+              started:
+                type: boolean
+              storageIP:
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+    longhorn-manager: ""
+  name: settings.longhorn.io
+spec:
+  group: longhorn.io
+  names:
+    kind: Setting
+    listKind: SettingList
+    plural: settings
+    shortNames:
+    - lhs
+    singular: setting
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - description: The value of the setting
+      jsonPath: .value
+      name: Value
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: Setting is where Longhorn stores setting object.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          value:
+            type: string
+        required:
+        - value
+        type: object
+    served: true
+    storage: false
+    subresources:
+      status: {}
+  - additionalPrinterColumns:
+    - description: The value of the setting
+      jsonPath: .value
+      name: Value
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v1beta2
+    schema:
+      openAPIV3Schema:
+        description: Setting is where Longhorn stores setting object.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          value:
+            type: string
+        required:
+        - value
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+    longhorn-manager: ""
+  name: sharemanagers.longhorn.io
+spec:
+  group: longhorn.io
+  names:
+    kind: ShareManager
+    listKind: ShareManagerList
+    plural: sharemanagers
+    shortNames:
+    - lhsm
+    singular: sharemanager
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - description: The state of the share manager
+      jsonPath: .status.state
+      name: State
+      type: string
+    - description: The node that the share manager is owned by
+      jsonPath: .status.ownerID
+      name: Node
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: ShareManager is where Longhorn stores share manager object.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            x-kubernetes-preserve-unknown-fields: true
+          status:
+            x-kubernetes-preserve-unknown-fields: true
+        type: object
+    served: true
+    storage: false
+    subresources:
+      status: {}
+  - additionalPrinterColumns:
+    - description: The state of the share manager
+      jsonPath: .status.state
+      name: State
+      type: string
+    - description: The node that the share manager is owned by
+      jsonPath: .status.ownerID
+      name: Node
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v1beta2
+    schema:
+      openAPIV3Schema:
+        description: ShareManager is where Longhorn stores share manager object.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: ShareManagerSpec defines the desired state of the Longhorn share manager
+            properties:
+              image:
+                type: string
+            type: object
+          status:
+            description: ShareManagerStatus defines the observed state of the Longhorn share manager
+            properties:
+              endpoint:
+                type: string
+              ownerID:
+                type: string
+              state:
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+    longhorn-manager: ""
+  name: snapshots.longhorn.io
+spec:
+  group: longhorn.io
+  names:
+    kind: Snapshot
+    listKind: SnapshotList
+    plural: snapshots
+    shortNames:
+    - lhsnap
+    singular: snapshot
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - description: The volume that this snapshot belongs to
+      jsonPath: .spec.volume
+      name: Volume
+      type: string
+    - description: Timestamp when the point-in-time snapshot was taken
+      jsonPath: .status.creationTime
+      name: CreationTime
+      type: string
+    - description: Indicates if the snapshot is ready to be used to restore/backup a volume
+      jsonPath: .status.readyToUse
+      name: ReadyToUse
+      type: boolean
+    - description: Represents the minimum size of volume required to rehydrate from this snapshot
+      jsonPath: .status.restoreSize
+      name: RestoreSize
+      type: string
+    - description: The actual size of the snapshot
+      jsonPath: .status.size
+      name: Size
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v1beta2
+    schema:
+      openAPIV3Schema:
+        description: Snapshot is the Schema for the snapshots API
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: SnapshotSpec defines the desired state of Longhorn Snapshot
+            properties:
+              createSnapshot:
+                description: require creating a new snapshot
+                type: boolean
+              labels:
+                additionalProperties:
+                  type: string
+                description: The labels of snapshot
+                nullable: true
+                type: object
+              volume:
+                description: the volume that this snapshot belongs to. This field is immutable after creation. Required
+                type: string
+            required:
+            - volume
+            type: object
+          status:
+            description: SnapshotStatus defines the observed state of Longhorn Snapshot
+            properties:
+              checksum:
+                type: string
+              children:
+                additionalProperties:
+                  type: boolean
+                nullable: true
+                type: object
+              creationTime:
+                type: string
+              error:
+                type: string
+              labels:
+                additionalProperties:
+                  type: string
+                nullable: true
+                type: object
+              markRemoved:
+                type: boolean
+              ownerID:
+                type: string
+              parent:
+                type: string
+              readyToUse:
+                type: boolean
+              restoreSize:
+                format: int64
+                type: integer
+              size:
+                format: int64
+                type: integer
+              userCreated:
+                type: boolean
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+    longhorn-manager: ""
+  name: supportbundles.longhorn.io
+spec:
+  group: longhorn.io
+  names:
+    kind: SupportBundle
+    listKind: SupportBundleList
+    plural: supportbundles
+    shortNames:
+    - lhbundle
+    singular: supportbundle
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - description: The state of the support bundle
+      jsonPath: .status.state
+      name: State
+      type: string
+    - description: The issue URL
+      jsonPath: .spec.issueURL
+      name: Issue
+      type: string
+    - description: A brief description of the issue
+      jsonPath: .spec.description
+      name: Description
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v1beta2
+    schema:
+      openAPIV3Schema:
+        description: SupportBundle is where Longhorn stores support bundle object
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: SupportBundleSpec defines the desired state of the Longhorn SupportBundle
+            properties:
+              description:
+                description: A brief description of the issue
+                type: string
+              issueURL:
+                description: The issue URL
+                nullable: true
+                type: string
+              nodeID:
+                description: The preferred responsible controller node ID.
+                type: string
+            required:
+            - description
+            type: object
+          status:
+            description: SupportBundleStatus defines the observed state of the Longhorn SupportBundle
+            properties:
+              conditions:
+                items:
+                  properties:
+                    lastProbeTime:
+                      description: Last time we probed the condition.
+                      type: string
+                    lastTransitionTime:
+                      description: Last time the condition transitioned from one status to another.
+                      type: string
+                    message:
+                      description: Human-readable message indicating details about last transition.
+                      type: string
+                    reason:
+                      description: Unique, one-word, CamelCase reason for the condition's last transition.
+                      type: string
+                    status:
+                      description: Status is the status of the condition. Can be True, False, Unknown.
+                      type: string
+                    type:
+                      description: Type is the type of the condition.
+                      type: string
+                  type: object
+                type: array
+              filename:
+                type: string
+              filesize:
+                format: int64
+                type: integer
+              image:
+                description: The support bundle manager image
+                type: string
+              managerIP:
+                description: The support bundle manager IP
+                type: string
+              ownerID:
+                description: The current responsible controller node ID
+                type: string
+              progress:
+                type: integer
+              state:
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+    longhorn-manager: ""
+  name: systembackups.longhorn.io
+spec:
+  group: longhorn.io
+  names:
+    kind: SystemBackup
+    listKind: SystemBackupList
+    plural: systembackups
+    shortNames:
+    - lhsb
+    singular: systembackup
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - description: The system backup Longhorn version
+      jsonPath: .status.version
+      name: Version
+      type: string
+    - description: The system backup state
+      jsonPath: .status.state
+      name: State
+      type: string
+    - description: The system backup creation time
+      jsonPath: .status.createdAt
+      name: Created
+      type: string
+    - description: The last time that the system backup was synced into the cluster
+      jsonPath: .status.lastSyncedAt
+      name: LastSyncedAt
+      type: string
+    name: v1beta2
+    schema:
+      openAPIV3Schema:
+        description: SystemBackup is where Longhorn stores system backup object
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: SystemBackupSpec defines the desired state of the Longhorn SystemBackup
+            type: object
+          status:
+            description: SystemBackupStatus defines the observed state of the Longhorn SystemBackup
+            properties:
+              conditions:
+                items:
+                  properties:
+                    lastProbeTime:
+                      description: Last time we probed the condition.
+                      type: string
+                    lastTransitionTime:
+                      description: Last time the condition transitioned from one status to another.
+                      type: string
+                    message:
+                      description: Human-readable message indicating details about last transition.
+                      type: string
+                    reason:
+                      description: Unique, one-word, CamelCase reason for the condition's last transition.
+                      type: string
+                    status:
+                      description: Status is the status of the condition. Can be True, False, Unknown.
+                      type: string
+                    type:
+                      description: Type is the type of the condition.
+                      type: string
+                  type: object
+                nullable: true
+                type: array
+              createdAt:
+                description: The system backup creation time.
+                format: date-time
+                type: string
+              gitCommit:
+                description: The saved Longhorn manager git commit.
+                nullable: true
+                type: string
+              lastSyncedAt:
+                description: The last time that the system backup was synced into the cluster.
+                format: date-time
+                nullable: true
+                type: string
+              managerImage:
+                description: The saved manager image.
+                type: string
+              ownerID:
+                description: The node ID of the responsible controller to reconcile this SystemBackup.
+                type: string
+              state:
+                description: The system backup state.
+                type: string
+              version:
+                description: The saved Longhorn version.
+                nullable: true
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+    longhorn-manager: ""
+  name: systemrestores.longhorn.io
+spec:
+  group: longhorn.io
+  names:
+    kind: SystemRestore
+    listKind: SystemRestoreList
+    plural: systemrestores
+    shortNames:
+    - lhsr
+    singular: systemrestore
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - description: The system restore state
+      jsonPath: .status.state
+      name: State
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v1beta2
+    schema:
+      openAPIV3Schema:
+        description: SystemRestore is where Longhorn stores system restore object
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: SystemRestoreSpec defines the desired state of the Longhorn SystemRestore
+            properties:
+              systemBackup:
+                description: The system backup name in the object store.
+                type: string
+            required:
+            - systemBackup
+            type: object
+          status:
+            description: SystemRestoreStatus defines the observed state of the Longhorn SystemRestore
+            properties:
+              conditions:
+                items:
+                  properties:
+                    lastProbeTime:
+                      description: Last time we probed the condition.
+                      type: string
+                    lastTransitionTime:
+                      description: Last time the condition transitioned from one status to another.
+                      type: string
+                    message:
+                      description: Human-readable message indicating details about last transition.
+                      type: string
+                    reason:
+                      description: Unique, one-word, CamelCase reason for the condition's last transition.
+                      type: string
+                    status:
+                      description: Status is the status of the condition. Can be True, False, Unknown.
+                      type: string
+                    type:
+                      description: Type is the type of the condition.
+                      type: string
+                  type: object
+                nullable: true
+                type: array
+              ownerID:
+                description: The node ID of the responsible controller to reconcile this SystemRestore.
+                type: string
+              sourceURL:
+                description: The source system backup URL.
+                type: string
+              state:
+                description: The system restore state.
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+    longhorn-manager: ""
+  name: volumes.longhorn.io
+spec:
+  preserveUnknownFields: false
+  conversion:
+    strategy: Webhook
+    webhook:
+      clientConfig:
+        service:
+          name: longhorn-conversion-webhook
+          namespace: {{ include "release_namespace" . }}
+          path: /v1/webhook/conversion
+          port: 9443
+      conversionReviewVersions:
+      - v1beta2
+      - v1beta1
+  group: longhorn.io
+  names:
+    kind: Volume
+    listKind: VolumeList
+    plural: volumes
+    shortNames:
+    - lhv
+    singular: volume
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - description: The state of the volume
+      jsonPath: .status.state
+      name: State
+      type: string
+    - description: The robustness of the volume
+      jsonPath: .status.robustness
+      name: Robustness
+      type: string
+    - description: The scheduled condition of the volume
+      jsonPath: .status.conditions['scheduled']['status']
+      name: Scheduled
+      type: string
+    - description: The size of the volume
+      jsonPath: .spec.size
+      name: Size
+      type: string
+    - description: The node that the volume is currently attaching to
+      jsonPath: .status.currentNodeID
+      name: Node
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: Volume is where Longhorn stores volume object.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            x-kubernetes-preserve-unknown-fields: true
+          status:
+            x-kubernetes-preserve-unknown-fields: true
+        type: object
+    served: true
+    storage: false
+    subresources:
+      status: {}
+  - additionalPrinterColumns:
+    - description: The state of the volume
+      jsonPath: .status.state
+      name: State
+      type: string
+    - description: The robustness of the volume
+      jsonPath: .status.robustness
+      name: Robustness
+      type: string
+    - description: The scheduled condition of the volume
+      jsonPath: .status.conditions[?(@.type=='Schedulable')].status
+      name: Scheduled
+      type: string
+    - description: The size of the volume
+      jsonPath: .spec.size
+      name: Size
+      type: string
+    - description: The node that the volume is currently attaching to
+      jsonPath: .status.currentNodeID
+      name: Node
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v1beta2
+    schema:
+      openAPIV3Schema:
+        description: Volume is where Longhorn stores volume object.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: VolumeSpec defines the desired state of the Longhorn volume
+            properties:
+              Standby:
+                type: boolean
+              accessMode:
+                enum:
+                - rwo
+                - rwx
+                type: string
+              backingImage:
+                type: string
+              baseImage:
+                description: Deprecated. Rename to BackingImage
+                type: string
+              dataLocality:
+                enum:
+                - disabled
+                - best-effort
+                - strict-local
+                type: string
+              dataSource:
+                type: string
+              disableFrontend:
+                type: boolean
+              diskSelector:
+                items:
+                  type: string
+                type: array
+              encrypted:
+                type: boolean
+              engineImage:
+                type: string
+              fromBackup:
+                type: string
+              restoreVolumeRecurringJob:
+                enum:
+                - ignored
+                - enabled
+                - disabled
+                type: string
+              frontend:
+                enum:
+                - blockdev
+                - iscsi
+                - ""
+                type: string
+              lastAttachedBy:
+                type: string
+              migratable:
+                type: boolean
+              migrationNodeID:
+                type: string
+              nodeID:
+                type: string
+              nodeSelector:
+                items:
+                  type: string
+                type: array
+              numberOfReplicas:
+                type: integer
+              recurringJobs:
+                description: Deprecated. Replaced by a separate resource named "RecurringJob"
+                items:
+                  description: 'Deprecated: This field is useless and has been replaced by the RecurringJob CRD'
+                  properties:
+                    concurrency:
+                      type: integer
+                    cron:
+                      type: string
+                    groups:
+                      items:
+                        type: string
+                      type: array
+                    labels:
+                      additionalProperties:
+                        type: string
+                      type: object
+                    name:
+                      type: string
+                    retain:
+                      type: integer
+                    task:
+                      enum:
+                      - snapshot
+                      - snapshot-cleanup
+                      - snapshot-delete
+                      - backup
+                      type: string
+                  type: object
+                type: array
+              replicaAutoBalance:
+                enum:
+                - ignored
+                - disabled
+                - least-effort
+                - best-effort
+                type: string
+              revisionCounterDisabled:
+                type: boolean
+              size:
+                format: int64
+                type: string
+              snapshotDataIntegrity:
+                enum:
+                - ignored
+                - disabled
+                - enabled
+                - fast-check
+                type: string
+              staleReplicaTimeout:
+                type: integer
+              unmapMarkSnapChainRemoved:
+                enum:
+                - ignored
+                - disabled
+                - enabled
+                type: string
+            type: object
+          status:
+            description: VolumeStatus defines the observed state of the Longhorn volume
+            properties:
+              actualSize:
+                format: int64
+                type: integer
+              cloneStatus:
+                properties:
+                  snapshot:
+                    type: string
+                  sourceVolume:
+                    type: string
+                  state:
+                    type: string
+                type: object
+              conditions:
+                items:
+                  properties:
+                    lastProbeTime:
+                      description: Last time we probed the condition.
+                      type: string
+                    lastTransitionTime:
+                      description: Last time the condition transitioned from one status to another.
+                      type: string
+                    message:
+                      description: Human-readable message indicating details about last transition.
+                      type: string
+                    reason:
+                      description: Unique, one-word, CamelCase reason for the condition's last transition.
+                      type: string
+                    status:
+                      description: Status is the status of the condition. Can be True, False, Unknown.
+                      type: string
+                    type:
+                      description: Type is the type of the condition.
+                      type: string
+                  type: object
+                nullable: true
+                type: array
+              currentImage:
+                type: string
+              currentNodeID:
+                type: string
+              expansionRequired:
+                type: boolean
+              frontendDisabled:
+                type: boolean
+              isStandby:
+                type: boolean
+              kubernetesStatus:
+                properties:
+                  lastPVCRefAt:
+                    type: string
+                  lastPodRefAt:
+                    type: string
+                  namespace:
+                    description: determine if PVC/Namespace is history or not
+                    type: string
+                  pvName:
+                    type: string
+                  pvStatus:
+                    type: string
+                  pvcName:
+                    type: string
+                  workloadsStatus:
+                    description: determine if Pod/Workload is history or not
+                    items:
+                      properties:
+                        podName:
+                          type: string
+                        podStatus:
+                          type: string
+                        workloadName:
+                          type: string
+                        workloadType:
+                          type: string
+                      type: object
+                    nullable: true
+                    type: array
+                type: object
+              lastBackup:
+                type: string
+              lastBackupAt:
+                type: string
+              lastDegradedAt:
+                type: string
+              ownerID:
+                type: string
+              pendingNodeID:
+                type: string
+              remountRequestedAt:
+                type: string
+              restoreInitiated:
+                type: boolean
+              restoreRequired:
+                type: boolean
+              robustness:
+                type: string
+              shareEndpoint:
+                type: string
+              shareState:
+                type: string
+              state:
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
diff --git a/charts/longhorn-1.4.1/templates/daemonset-sa.yaml b/charts/longhorn-1.4.1/templates/daemonset-sa.yaml
new file mode 100644
index 0000000..63f98cd
--- /dev/null
+++ b/charts/longhorn-1.4.1/templates/daemonset-sa.yaml
@@ -0,0 +1,147 @@
+apiVersion: apps/v1
+kind: DaemonSet
+metadata:
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+    app: longhorn-manager
+  name: longhorn-manager
+  namespace: {{ include "release_namespace" . }}
+spec:
+  selector:
+    matchLabels:
+      app: longhorn-manager
+  template:
+    metadata:
+      labels: {{- include "longhorn.labels" . | nindent 8 }}
+        app: longhorn-manager
+      {{- with .Values.annotations }}
+      annotations:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+    spec:
+      initContainers:
+      - name: wait-longhorn-admission-webhook
+        image: {{ template "registry_url" . }}{{ .Values.image.longhorn.manager.repository }}:{{ .Values.image.longhorn.manager.tag }}
+        command: ['sh', '-c', 'while [ $(curl -m 1 -s -o /dev/null -w "%{http_code}" -k https://longhorn-admission-webhook:9443/v1/healthz) != "200" ]; do echo waiting; sleep 2; done']
+      containers:
+      - name: longhorn-manager
+        image: {{ template "registry_url" . }}{{ .Values.image.longhorn.manager.repository }}:{{ .Values.image.longhorn.manager.tag }}
+        imagePullPolicy: {{ .Values.image.pullPolicy }}
+        securityContext:
+          privileged: true
+        command:
+        - longhorn-manager
+        - -d
+        {{- if eq .Values.longhornManager.log.format "json" }}
+        - -j
+        {{- end }}
+        - daemon
+        - --engine-image
+        - "{{ template "registry_url" . }}{{ .Values.image.longhorn.engine.repository }}:{{ .Values.image.longhorn.engine.tag }}"
+        - --instance-manager-image
+        - "{{ template "registry_url" . }}{{ .Values.image.longhorn.instanceManager.repository }}:{{ .Values.image.longhorn.instanceManager.tag }}"
+        - --share-manager-image
+        - "{{ template "registry_url" . }}{{ .Values.image.longhorn.shareManager.repository }}:{{ .Values.image.longhorn.shareManager.tag }}"
+        - --backing-image-manager-image
+        - "{{ template "registry_url" . }}{{ .Values.image.longhorn.backingImageManager.repository }}:{{ .Values.image.longhorn.backingImageManager.tag }}"
+        - --support-bundle-manager-image
+        - "{{ template "registry_url" . }}{{ .Values.image.longhorn.supportBundleKit.repository }}:{{ .Values.image.longhorn.supportBundleKit.tag }}"
+        - --manager-image
+        - "{{ template "registry_url" . }}{{ .Values.image.longhorn.manager.repository }}:{{ .Values.image.longhorn.manager.tag }}"
+        - --service-account
+        - longhorn-service-account
+        ports:
+        - containerPort: 9500
+          name: manager
+        readinessProbe:
+          tcpSocket:
+            port: 9500
+        volumeMounts:
+        - name: dev
+          mountPath: /host/dev/
+        - name: proc
+          mountPath: /host/proc/
+        - name: longhorn
+          mountPath: /var/lib/longhorn/
+          mountPropagation: Bidirectional
+        - name: longhorn-grpc-tls
+          mountPath: /tls-files/
+        env:
+        - name: POD_NAMESPACE
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.namespace
+        - name: POD_IP
+          valueFrom:
+            fieldRef:
+              fieldPath: status.podIP
+        - name: NODE_NAME
+          valueFrom:
+            fieldRef:
+              fieldPath: spec.nodeName
+      volumes:
+      - name: dev
+        hostPath:
+          path: /dev/
+      - name: proc
+        hostPath:
+          path: /proc/
+      - name: longhorn
+        hostPath:
+          path: /var/lib/longhorn/
+      - name: longhorn-grpc-tls
+        secret:
+          secretName: longhorn-grpc-tls
+          optional: true
+      {{- if .Values.privateRegistry.registrySecret }}
+      imagePullSecrets:
+      - name: {{ .Values.privateRegistry.registrySecret }}
+      {{- end }}
+      {{- if .Values.longhornManager.priorityClass }}
+      priorityClassName: {{ .Values.longhornManager.priorityClass | quote }}
+      {{- end }}
+      {{- if or .Values.longhornManager.tolerations .Values.global.cattle.windowsCluster.enabled }}
+      tolerations:
+        {{- if and .Values.global.cattle.windowsCluster.enabled .Values.global.cattle.windowsCluster.tolerations }}
+{{ toYaml .Values.global.cattle.windowsCluster.tolerations | indent 6 }}
+        {{- end }}
+        {{- if .Values.longhornManager.tolerations }}
+{{ toYaml .Values.longhornManager.tolerations | indent 6 }}
+        {{- end }}
+      {{- end }}
+      {{- if or .Values.longhornManager.nodeSelector .Values.global.cattle.windowsCluster.enabled }}
+      nodeSelector:
+        {{- if and .Values.global.cattle.windowsCluster.enabled .Values.global.cattle.windowsCluster.nodeSelector }}
+{{ toYaml .Values.global.cattle.windowsCluster.nodeSelector | indent 8 }}
+        {{- end }}
+        {{- if .Values.longhornManager.nodeSelector }}
+{{ toYaml .Values.longhornManager.nodeSelector | indent 8 }}
+        {{- end }}
+      {{- end }}
+      serviceAccountName: longhorn-service-account
+  updateStrategy:
+    rollingUpdate:
+      maxUnavailable: "100%"
+---
+apiVersion: v1
+kind: Service
+metadata:
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+    app: longhorn-manager
+  name: longhorn-backend
+  namespace: {{ include "release_namespace" . }}
+  {{- if .Values.longhornManager.serviceAnnotations }}
+  annotations:
+{{ toYaml .Values.longhornManager.serviceAnnotations | indent 4 }}
+  {{- end }}
+spec:
+  type: {{ .Values.service.manager.type }}
+  sessionAffinity: ClientIP
+  selector:
+    app: longhorn-manager
+  ports:
+  - name: manager
+    port: 9500
+    targetPort: manager
+    {{- if .Values.service.manager.nodePort }}
+    nodePort: {{ .Values.service.manager.nodePort }}
+    {{- end }}
diff --git a/charts/longhorn-1.4.1/templates/default-setting.yaml b/charts/longhorn-1.4.1/templates/default-setting.yaml
new file mode 100644
index 0000000..49870a4
--- /dev/null
+++ b/charts/longhorn-1.4.1/templates/default-setting.yaml
@@ -0,0 +1,79 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: longhorn-default-setting
+  namespace: {{ include "release_namespace" . }}
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+data:
+  default-setting.yaml: |-
+    {{ if not (kindIs "invalid" .Values.defaultSettings.backupTarget) }}backup-target: {{ .Values.defaultSettings.backupTarget }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.backupTargetCredentialSecret) }}backup-target-credential-secret: {{ .Values.defaultSettings.backupTargetCredentialSecret }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.allowRecurringJobWhileVolumeDetached) }}allow-recurring-job-while-volume-detached: {{ .Values.defaultSettings.allowRecurringJobWhileVolumeDetached }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.createDefaultDiskLabeledNodes) }}create-default-disk-labeled-nodes: {{ .Values.defaultSettings.createDefaultDiskLabeledNodes }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.defaultDataPath) }}default-data-path: {{ .Values.defaultSettings.defaultDataPath }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.replicaSoftAntiAffinity) }}replica-soft-anti-affinity: {{ .Values.defaultSettings.replicaSoftAntiAffinity }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.replicaAutoBalance) }}replica-auto-balance: {{ .Values.defaultSettings.replicaAutoBalance }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.storageOverProvisioningPercentage) }}storage-over-provisioning-percentage: {{ .Values.defaultSettings.storageOverProvisioningPercentage }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.storageMinimalAvailablePercentage) }}storage-minimal-available-percentage: {{ .Values.defaultSettings.storageMinimalAvailablePercentage }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.upgradeChecker) }}upgrade-checker: {{ .Values.defaultSettings.upgradeChecker }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.defaultReplicaCount) }}default-replica-count: {{ .Values.defaultSettings.defaultReplicaCount }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.defaultDataLocality) }}default-data-locality: {{ .Values.defaultSettings.defaultDataLocality }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.defaultLonghornStaticStorageClass) }}default-longhorn-static-storage-class: {{ .Values.defaultSettings.defaultLonghornStaticStorageClass }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.backupstorePollInterval) }}backupstore-poll-interval: {{ .Values.defaultSettings.backupstorePollInterval }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.failedBackupTTL) }}failed-backup-ttl: {{ .Values.defaultSettings.failedBackupTTL }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.restoreVolumeRecurringJobs) }}restore-volume-recurring-jobs: {{ .Values.defaultSettings.restoreVolumeRecurringJobs }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.recurringSuccessfulJobsHistoryLimit) }}recurring-successful-jobs-history-limit: {{ .Values.defaultSettings.recurringSuccessfulJobsHistoryLimit }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.recurringFailedJobsHistoryLimit) }}recurring-failed-jobs-history-limit: {{ .Values.defaultSettings.recurringFailedJobsHistoryLimit }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.supportBundleFailedHistoryLimit) }}support-bundle-failed-history-limit: {{ .Values.defaultSettings.supportBundleFailedHistoryLimit }}{{ end }}
+    {{- if or (not (kindIs "invalid" .Values.defaultSettings.taintToleration)) (.Values.global.cattle.windowsCluster.enabled) }}
+    taint-toleration: {{ $windowsDefaultSettingTaintToleration := list }}{{ $defaultSettingTaintToleration := list -}}
+      {{- if and .Values.global.cattle.windowsCluster.enabled .Values.global.cattle.windowsCluster.defaultSetting.taintToleration -}}
+        {{- $windowsDefaultSettingTaintToleration = .Values.global.cattle.windowsCluster.defaultSetting.taintToleration -}}
+      {{- end -}}
+      {{- if not (kindIs "invalid" .Values.defaultSettings.taintToleration) -}}
+        {{- $defaultSettingTaintToleration = .Values.defaultSettings.taintToleration -}}
+      {{- end -}}
+      {{- $taintToleration := list $windowsDefaultSettingTaintToleration $defaultSettingTaintToleration }}{{ join ";" (compact $taintToleration) -}}
+    {{- end }}
+    {{- if or (not (kindIs "invalid" .Values.defaultSettings.systemManagedComponentsNodeSelector)) (.Values.global.cattle.windowsCluster.enabled) }}
+    system-managed-components-node-selector: {{ $windowsDefaultSettingNodeSelector := list }}{{ $defaultSettingNodeSelector := list -}}
+      {{- if and .Values.global.cattle.windowsCluster.enabled .Values.global.cattle.windowsCluster.defaultSetting.systemManagedComponentsNodeSelector -}}
+        {{ $windowsDefaultSettingNodeSelector = .Values.global.cattle.windowsCluster.defaultSetting.systemManagedComponentsNodeSelector -}}
+      {{- end -}}
+      {{- if not (kindIs "invalid" .Values.defaultSettings.systemManagedComponentsNodeSelector) -}}
+        {{- $defaultSettingNodeSelector = .Values.defaultSettings.systemManagedComponentsNodeSelector -}}
+      {{- end -}}
+      {{- $nodeSelector := list $windowsDefaultSettingNodeSelector $defaultSettingNodeSelector }}{{ join ";" (compact $nodeSelector) -}}
+    {{- end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.priorityClass) }}priority-class: {{ .Values.defaultSettings.priorityClass }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.autoSalvage) }}auto-salvage: {{ .Values.defaultSettings.autoSalvage }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.autoDeletePodWhenVolumeDetachedUnexpectedly) }}auto-delete-pod-when-volume-detached-unexpectedly: {{ .Values.defaultSettings.autoDeletePodWhenVolumeDetachedUnexpectedly }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.disableSchedulingOnCordonedNode) }}disable-scheduling-on-cordoned-node: {{ .Values.defaultSettings.disableSchedulingOnCordonedNode }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.replicaZoneSoftAntiAffinity) }}replica-zone-soft-anti-affinity: {{ .Values.defaultSettings.replicaZoneSoftAntiAffinity }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.nodeDownPodDeletionPolicy) }}node-down-pod-deletion-policy: {{ .Values.defaultSettings.nodeDownPodDeletionPolicy }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.allowNodeDrainWithLastHealthyReplica) }}allow-node-drain-with-last-healthy-replica: {{ .Values.defaultSettings.allowNodeDrainWithLastHealthyReplica }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.mkfsExt4Parameters) }}mkfs-ext4-parameters: {{ .Values.defaultSettings.mkfsExt4Parameters }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.disableReplicaRebuild) }}disable-replica-rebuild: {{ .Values.defaultSettings.disableReplicaRebuild }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.replicaReplenishmentWaitInterval) }}replica-replenishment-wait-interval: {{ .Values.defaultSettings.replicaReplenishmentWaitInterval }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.concurrentReplicaRebuildPerNodeLimit) }}concurrent-replica-rebuild-per-node-limit: {{ .Values.defaultSettings.concurrentReplicaRebuildPerNodeLimit }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.concurrentVolumeBackupRestorePerNodeLimit) }}concurrent-volume-backup-restore-per-node-limit: {{ .Values.defaultSettings.concurrentVolumeBackupRestorePerNodeLimit }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.disableRevisionCounter) }}disable-revision-counter: {{ .Values.defaultSettings.disableRevisionCounter }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.systemManagedPodsImagePullPolicy) }}system-managed-pods-image-pull-policy: {{ .Values.defaultSettings.systemManagedPodsImagePullPolicy }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.allowVolumeCreationWithDegradedAvailability) }}allow-volume-creation-with-degraded-availability: {{ .Values.defaultSettings.allowVolumeCreationWithDegradedAvailability }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.autoCleanupSystemGeneratedSnapshot) }}auto-cleanup-system-generated-snapshot: {{ .Values.defaultSettings.autoCleanupSystemGeneratedSnapshot }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.concurrentAutomaticEngineUpgradePerNodeLimit) }}concurrent-automatic-engine-upgrade-per-node-limit: {{ .Values.defaultSettings.concurrentAutomaticEngineUpgradePerNodeLimit }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.backingImageCleanupWaitInterval) }}backing-image-cleanup-wait-interval: {{ .Values.defaultSettings.backingImageCleanupWaitInterval }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.backingImageRecoveryWaitInterval) }}backing-image-recovery-wait-interval: {{ .Values.defaultSettings.backingImageRecoveryWaitInterval }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.guaranteedEngineManagerCPU) }}guaranteed-engine-manager-cpu: {{ .Values.defaultSettings.guaranteedEngineManagerCPU }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.guaranteedReplicaManagerCPU) }}guaranteed-replica-manager-cpu: {{ .Values.defaultSettings.guaranteedReplicaManagerCPU }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.kubernetesClusterAutoscalerEnabled) }}kubernetes-cluster-autoscaler-enabled: {{ .Values.defaultSettings.kubernetesClusterAutoscalerEnabled }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.orphanAutoDeletion) }}orphan-auto-deletion: {{ .Values.defaultSettings.orphanAutoDeletion }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.storageNetwork) }}storage-network: {{ .Values.defaultSettings.storageNetwork }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.deletingConfirmationFlag) }}deleting-confirmation-flag: {{ .Values.defaultSettings.deletingConfirmationFlag }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.engineReplicaTimeout) }}engine-replica-timeout: {{ .Values.defaultSettings.engineReplicaTimeout }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.snapshotDataIntegrity) }}snapshot-data-integrity: {{ .Values.defaultSettings.snapshotDataIntegrity }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.snapshotDataIntegrityImmediateCheckAfterSnapshotCreation) }}snapshot-data-integrity-immediate-check-after-snapshot-creation: {{ .Values.defaultSettings.snapshotDataIntegrityImmediateCheckAfterSnapshotCreation }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.snapshotDataIntegrityCronjob) }}snapshot-data-integrity-cronjob: {{ .Values.defaultSettings.snapshotDataIntegrityCronjob }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.removeSnapshotsDuringFilesystemTrim) }}remove-snapshots-during-filesystem-trim: {{ .Values.defaultSettings.removeSnapshotsDuringFilesystemTrim }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.fastReplicaRebuildEnabled) }}fast-replica-rebuild-enabled: {{ .Values.defaultSettings.fastReplicaRebuildEnabled }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.replicaFileSyncHttpClientTimeout) }}replica-file-sync-http-client-timeout: {{ .Values.defaultSettings.replicaFileSyncHttpClientTimeout }}{{ end }}
\ No newline at end of file
diff --git a/charts/longhorn-1.4.1/templates/deployment-driver.yaml b/charts/longhorn-1.4.1/templates/deployment-driver.yaml
new file mode 100644
index 0000000..f162fbf
--- /dev/null
+++ b/charts/longhorn-1.4.1/templates/deployment-driver.yaml
@@ -0,0 +1,118 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: longhorn-driver-deployer
+  namespace: {{ include "release_namespace" . }}
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: longhorn-driver-deployer
+  template:
+    metadata:
+      labels: {{- include "longhorn.labels" . | nindent 8 }}
+        app: longhorn-driver-deployer
+    spec:
+      initContainers:
+        - name: wait-longhorn-manager
+          image: {{ template "registry_url" . }}{{ .Values.image.longhorn.manager.repository }}:{{ .Values.image.longhorn.manager.tag }}
+          command: ['sh', '-c', 'while [ $(curl -m 1 -s -o /dev/null -w "%{http_code}" http://longhorn-backend:9500/v1) != "200" ]; do echo waiting; sleep 2; done']
+      containers:
+        - name: longhorn-driver-deployer
+          image: {{ template "registry_url" . }}{{ .Values.image.longhorn.manager.repository }}:{{ .Values.image.longhorn.manager.tag }}
+          imagePullPolicy: {{ .Values.image.pullPolicy }}
+          command:
+          - longhorn-manager
+          - -d
+          - deploy-driver
+          - --manager-image
+          - "{{ template "registry_url" . }}{{ .Values.image.longhorn.manager.repository }}:{{ .Values.image.longhorn.manager.tag }}"
+          - --manager-url
+          - http://longhorn-backend:9500/v1
+          env:
+          - name: POD_NAMESPACE
+            valueFrom:
+              fieldRef:
+                fieldPath: metadata.namespace
+          - name: NODE_NAME
+            valueFrom:
+              fieldRef:
+                fieldPath: spec.nodeName
+          - name: SERVICE_ACCOUNT
+            valueFrom:
+              fieldRef:
+                fieldPath: spec.serviceAccountName
+          {{- if .Values.csi.kubeletRootDir }}
+          - name: KUBELET_ROOT_DIR
+            value: {{ .Values.csi.kubeletRootDir }}
+          {{- end }}
+          {{- if and .Values.image.csi.attacher.repository .Values.image.csi.attacher.tag }}
+          - name: CSI_ATTACHER_IMAGE
+            value: "{{ template "registry_url" . }}{{ .Values.image.csi.attacher.repository }}:{{ .Values.image.csi.attacher.tag }}"
+          {{- end }}
+          {{- if and .Values.image.csi.provisioner.repository .Values.image.csi.provisioner.tag }}
+          - name: CSI_PROVISIONER_IMAGE
+            value: "{{ template "registry_url" . }}{{ .Values.image.csi.provisioner.repository }}:{{ .Values.image.csi.provisioner.tag }}"
+          {{- end }}
+          {{- if and .Values.image.csi.nodeDriverRegistrar.repository .Values.image.csi.nodeDriverRegistrar.tag }}
+          - name: CSI_NODE_DRIVER_REGISTRAR_IMAGE
+            value: "{{ template "registry_url" . }}{{ .Values.image.csi.nodeDriverRegistrar.repository }}:{{ .Values.image.csi.nodeDriverRegistrar.tag }}"
+          {{- end }}
+          {{- if and .Values.image.csi.resizer.repository .Values.image.csi.resizer.tag }}
+          - name: CSI_RESIZER_IMAGE
+            value: "{{ template "registry_url" . }}{{ .Values.image.csi.resizer.repository }}:{{ .Values.image.csi.resizer.tag }}"
+          {{- end }}
+          {{- if and .Values.image.csi.snapshotter.repository .Values.image.csi.snapshotter.tag }}
+          - name: CSI_SNAPSHOTTER_IMAGE
+            value: "{{ template "registry_url" . }}{{ .Values.image.csi.snapshotter.repository }}:{{ .Values.image.csi.snapshotter.tag }}"
+          {{- end }}
+          {{- if and .Values.image.csi.livenessProbe.repository .Values.image.csi.livenessProbe.tag }}
+          - name: CSI_LIVENESS_PROBE_IMAGE
+            value: "{{ template "registry_url" . }}{{ .Values.image.csi.livenessProbe.repository }}:{{ .Values.image.csi.livenessProbe.tag }}"
+          {{- end }}
+          {{- if .Values.csi.attacherReplicaCount }}
+          - name: CSI_ATTACHER_REPLICA_COUNT
+            value: {{ .Values.csi.attacherReplicaCount | quote }}
+          {{- end }}
+          {{- if .Values.csi.provisionerReplicaCount }}
+          - name: CSI_PROVISIONER_REPLICA_COUNT
+            value: {{ .Values.csi.provisionerReplicaCount | quote }}
+          {{- end }}
+          {{- if .Values.csi.resizerReplicaCount }}
+          - name: CSI_RESIZER_REPLICA_COUNT
+            value: {{ .Values.csi.resizerReplicaCount | quote }}
+          {{- end }}
+          {{- if .Values.csi.snapshotterReplicaCount }}
+          - name: CSI_SNAPSHOTTER_REPLICA_COUNT
+            value: {{ .Values.csi.snapshotterReplicaCount | quote }}
+          {{- end }}
+
+      {{- if .Values.privateRegistry.registrySecret }}
+      imagePullSecrets:
+      - name: {{ .Values.privateRegistry.registrySecret }}
+      {{- end }}
+      {{- if .Values.longhornDriver.priorityClass }}
+      priorityClassName: {{ .Values.longhornDriver.priorityClass | quote }}
+      {{- end }}
+      {{- if or .Values.longhornDriver.tolerations .Values.global.cattle.windowsCluster.enabled }}
+      tolerations:
+        {{- if and .Values.global.cattle.windowsCluster.enabled .Values.global.cattle.windowsCluster.tolerations }}
+{{ toYaml .Values.global.cattle.windowsCluster.tolerations | indent 6 }}
+        {{- end }}
+        {{- if .Values.longhornDriver.tolerations }}
+{{ toYaml .Values.longhornDriver.tolerations | indent 6 }}
+        {{- end }}
+      {{- end }}
+      {{- if or .Values.longhornDriver.nodeSelector .Values.global.cattle.windowsCluster.enabled }}
+      nodeSelector:
+        {{- if and .Values.global.cattle.windowsCluster.enabled .Values.global.cattle.windowsCluster.nodeSelector }}
+{{ toYaml .Values.global.cattle.windowsCluster.nodeSelector | indent 8 }}
+        {{- end }}
+        {{- if .Values.longhornDriver.nodeSelector }}
+{{ toYaml .Values.longhornDriver.nodeSelector | indent 8 }}
+        {{- end }}
+      {{- end }}
+      serviceAccountName: longhorn-service-account
+      securityContext:
+        runAsUser: 0
diff --git a/charts/longhorn/templates/deployment-recovery-backend.yaml b/charts/longhorn-1.4.1/templates/deployment-recovery-backend.yaml
similarity index 100%
rename from charts/longhorn/templates/deployment-recovery-backend.yaml
rename to charts/longhorn-1.4.1/templates/deployment-recovery-backend.yaml
diff --git a/charts/longhorn-1.4.1/templates/deployment-ui.yaml b/charts/longhorn-1.4.1/templates/deployment-ui.yaml
new file mode 100644
index 0000000..6bad5cd
--- /dev/null
+++ b/charts/longhorn-1.4.1/templates/deployment-ui.yaml
@@ -0,0 +1,114 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+    app: longhorn-ui
+  name: longhorn-ui
+  namespace: {{ include "release_namespace" . }}
+spec:
+  replicas: {{ .Values.longhornUI.replicas }}
+  selector:
+    matchLabels:
+      app: longhorn-ui
+  template:
+    metadata:
+      labels: {{- include "longhorn.labels" . | nindent 8 }}
+        app: longhorn-ui
+    spec:
+      affinity:
+        podAntiAffinity:
+          preferredDuringSchedulingIgnoredDuringExecution:
+          - weight: 1
+            podAffinityTerm:
+              labelSelector:
+                matchExpressions:
+                - key: app
+                  operator: In
+                  values:
+                  - longhorn-ui
+              topologyKey: kubernetes.io/hostname
+      containers:
+      - name: longhorn-ui
+        image: {{ template "registry_url" . }}{{ .Values.image.longhorn.ui.repository }}:{{ .Values.image.longhorn.ui.tag }}
+        imagePullPolicy: {{ .Values.image.pullPolicy }}
+        volumeMounts:
+        - name : nginx-cache
+          mountPath: /var/cache/nginx/
+        - name : nginx-config
+          mountPath: /var/config/nginx/
+        - name: var-run
+          mountPath: /var/run/
+        ports:
+        - containerPort: 8000
+          name: http
+        env:
+          - name: LONGHORN_MANAGER_IP
+            value: "http://longhorn-backend:9500"
+          - name: LONGHORN_UI_PORT
+            value: "8000"
+      volumes:
+      - emptyDir: {}
+        name: nginx-cache
+      - emptyDir: {}
+        name: nginx-config
+      - emptyDir: {}
+        name: var-run
+      {{- if .Values.privateRegistry.registrySecret }}
+      imagePullSecrets:
+      - name: {{ .Values.privateRegistry.registrySecret }}
+      {{- end }}
+      {{- if .Values.longhornUI.priorityClass }}
+      priorityClassName: {{ .Values.longhornUI.priorityClass | quote }}
+      {{- end }}
+      {{- if or .Values.longhornUI.tolerations .Values.global.cattle.windowsCluster.enabled }}
+      tolerations:
+        {{- if and .Values.global.cattle.windowsCluster.enabled .Values.global.cattle.windowsCluster.tolerations }}
+{{ toYaml .Values.global.cattle.windowsCluster.tolerations | indent 6 }}
+        {{- end }}
+        {{- if .Values.longhornUI.tolerations }}
+{{ toYaml .Values.longhornUI.tolerations | indent 6 }}
+        {{- end }}
+      {{- end }}
+      {{- if or .Values.longhornUI.nodeSelector .Values.global.cattle.windowsCluster.enabled }}
+      nodeSelector:
+        {{- if and .Values.global.cattle.windowsCluster.enabled .Values.global.cattle.windowsCluster.nodeSelector }}
+{{ toYaml .Values.global.cattle.windowsCluster.nodeSelector | indent 8 }}
+        {{- end }}
+        {{- if .Values.longhornUI.nodeSelector }}
+{{ toYaml .Values.longhornUI.nodeSelector | indent 8 }}
+        {{- end }}
+      {{- end }}
+---
+kind: Service
+apiVersion: v1
+metadata:
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+    app: longhorn-ui
+    {{- if eq .Values.service.ui.type "Rancher-Proxy" }}
+    kubernetes.io/cluster-service: "true"
+    {{- end }}
+  name: longhorn-frontend
+  namespace: {{ include "release_namespace" . }}
+spec:
+  {{- if eq .Values.service.ui.type "Rancher-Proxy" }}
+  type: ClusterIP
+  {{- else }}
+  type: {{ .Values.service.ui.type }}
+  {{- end }}
+  {{- if and .Values.service.ui.loadBalancerIP (eq .Values.service.ui.type "LoadBalancer") }}
+  loadBalancerIP: {{ .Values.service.ui.loadBalancerIP }}
+  {{- end }}
+  {{- if and (eq .Values.service.ui.type "LoadBalancer") .Values.service.ui.loadBalancerSourceRanges }}
+  loadBalancerSourceRanges: {{- toYaml .Values.service.ui.loadBalancerSourceRanges | nindent 4 }}
+  {{- end }}
+  selector:
+    app: longhorn-ui
+  ports:
+  - name: http
+    port: 80
+    targetPort: http
+    {{- if .Values.service.ui.nodePort }}
+    nodePort: {{ .Values.service.ui.nodePort }}
+    {{- else }}
+    nodePort: null
+    {{- end }}
diff --git a/charts/longhorn/templates/deployment-webhook.yaml b/charts/longhorn-1.4.1/templates/deployment-webhook.yaml
similarity index 100%
rename from charts/longhorn/templates/deployment-webhook.yaml
rename to charts/longhorn-1.4.1/templates/deployment-webhook.yaml
diff --git a/charts/longhorn-1.4.1/templates/ingress.yaml b/charts/longhorn-1.4.1/templates/ingress.yaml
new file mode 100644
index 0000000..ee47f8b
--- /dev/null
+++ b/charts/longhorn-1.4.1/templates/ingress.yaml
@@ -0,0 +1,48 @@
+{{- if .Values.ingress.enabled }}
+{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
+apiVersion: networking.k8s.io/v1
+{{- else -}}
+apiVersion: networking.k8s.io/v1beta1
+{{- end }}
+kind: Ingress
+metadata:
+  name: longhorn-ingress
+  namespace: {{ include "release_namespace" . }}
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+    app: longhorn-ingress
+  annotations:
+    {{- if .Values.ingress.secureBackends }}
+    ingress.kubernetes.io/secure-backends: "true"
+    {{- end }}
+    {{- range $key, $value := .Values.ingress.annotations }}
+    {{ $key }}: {{ $value | quote }}
+    {{- end }}
+spec:
+  {{- if and .Values.ingress.ingressClassName (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
+  ingressClassName: {{ .Values.ingress.ingressClassName }}
+  {{- end }}
+  rules:
+  - host: {{ .Values.ingress.host }}
+    http:
+      paths:
+        - path: {{ default "" .Values.ingress.path }}
+          {{- if (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
+          pathType: ImplementationSpecific
+          {{- end }}
+          backend:
+            {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
+            service:
+              name: longhorn-frontend
+              port:
+                number: 80
+            {{- else }}
+            serviceName: longhorn-frontend
+            servicePort: 80
+            {{- end }}
+{{- if .Values.ingress.tls }}
+  tls:
+  - hosts:
+    - {{ .Values.ingress.host }}
+    secretName: {{ .Values.ingress.tlsSecret }}
+{{- end }}
+{{- end }}
diff --git a/charts/longhorn-1.4.1/templates/postupgrade-job.yaml b/charts/longhorn-1.4.1/templates/postupgrade-job.yaml
new file mode 100644
index 0000000..b9b2eeb
--- /dev/null
+++ b/charts/longhorn-1.4.1/templates/postupgrade-job.yaml
@@ -0,0 +1,58 @@
+apiVersion: batch/v1
+kind: Job
+metadata:
+  annotations:
+    "helm.sh/hook": post-upgrade
+    "helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation
+  name: longhorn-post-upgrade
+  namespace: {{ include "release_namespace" . }}
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+spec:
+  activeDeadlineSeconds: 900
+  backoffLimit: 1
+  template:
+    metadata:
+      name: longhorn-post-upgrade
+      labels: {{- include "longhorn.labels" . | nindent 8 }}
+    spec:
+      containers:
+      - name: longhorn-post-upgrade
+        image: {{ template "registry_url" . }}{{ .Values.image.longhorn.manager.repository }}:{{ .Values.image.longhorn.manager.tag }}
+        imagePullPolicy: {{ .Values.image.pullPolicy }}
+        securityContext:
+          privileged: true
+        command:
+        - longhorn-manager
+        - post-upgrade
+        env:
+        - name: POD_NAMESPACE
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.namespace
+      restartPolicy: OnFailure
+      {{- if .Values.privateRegistry.registrySecret }}
+      imagePullSecrets:
+      - name: {{ .Values.privateRegistry.registrySecret }}
+      {{- end }}
+      {{- if .Values.longhornManager.priorityClass }}
+      priorityClassName: {{ .Values.longhornManager.priorityClass | quote }}
+      {{- end }}
+      serviceAccountName: longhorn-service-account
+      {{- if or .Values.longhornManager.tolerations .Values.global.cattle.windowsCluster.enabled }}
+      tolerations:
+        {{- if and .Values.global.cattle.windowsCluster.enabled .Values.global.cattle.windowsCluster.tolerations }}
+{{ toYaml .Values.global.cattle.windowsCluster.tolerations | indent 6 }}
+        {{- end }}
+        {{- if .Values.longhornManager.tolerations }}
+{{ toYaml .Values.longhornManager.tolerations | indent 6 }}
+        {{- end }}
+      {{- end }}
+      {{- if or .Values.longhornManager.nodeSelector .Values.global.cattle.windowsCluster.enabled }}
+      nodeSelector:
+        {{- if and .Values.global.cattle.windowsCluster.enabled .Values.global.cattle.windowsCluster.nodeSelector }}
+{{ toYaml .Values.global.cattle.windowsCluster.nodeSelector | indent 8 }}
+        {{- end }}
+        {{- if .Values.longhornManager.nodeSelector }}
+{{ toYaml .Values.longhornManager.nodeSelector | indent 8 }}
+        {{- end }}
+      {{- end }}
diff --git a/charts/longhorn-1.4.1/templates/psp.yaml b/charts/longhorn-1.4.1/templates/psp.yaml
new file mode 100644
index 0000000..a2dfc05
--- /dev/null
+++ b/charts/longhorn-1.4.1/templates/psp.yaml
@@ -0,0 +1,66 @@
+{{- if .Values.enablePSP }}
+apiVersion: policy/v1beta1
+kind: PodSecurityPolicy
+metadata:
+  name: longhorn-psp
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+spec:
+  privileged: true
+  allowPrivilegeEscalation: true
+  requiredDropCapabilities:
+  - NET_RAW
+  allowedCapabilities:
+  - SYS_ADMIN
+  hostNetwork: false
+  hostIPC: false
+  hostPID: true
+  runAsUser:
+    rule: RunAsAny
+  seLinux:
+    rule: RunAsAny
+  fsGroup:
+    rule: RunAsAny
+  supplementalGroups:
+    rule: RunAsAny
+  volumes:
+  - configMap
+  - downwardAPI
+  - emptyDir
+  - secret
+  - projected
+  - hostPath
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: longhorn-psp-role
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+  namespace: {{ include "release_namespace" . }}
+rules:
+- apiGroups:
+  - policy
+  resources:
+  - podsecuritypolicies
+  verbs:
+  - use
+  resourceNames:
+  - longhorn-psp
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  name: longhorn-psp-binding
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+  namespace: {{ include "release_namespace" . }}
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: longhorn-psp-role
+subjects:
+- kind: ServiceAccount
+  name: longhorn-service-account
+  namespace: {{ include "release_namespace" . }}
+- kind: ServiceAccount
+  name: default
+  namespace: {{ include "release_namespace" . }}
+{{- end }}
diff --git a/charts/longhorn-1.4.1/templates/registry-secret.yaml b/charts/longhorn-1.4.1/templates/registry-secret.yaml
new file mode 100644
index 0000000..3c6b1dc
--- /dev/null
+++ b/charts/longhorn-1.4.1/templates/registry-secret.yaml
@@ -0,0 +1,13 @@
+{{- if .Values.privateRegistry.createSecret }}
+{{- if .Values.privateRegistry.registrySecret }}
+apiVersion: v1
+kind: Secret
+metadata:
+  name: {{ .Values.privateRegistry.registrySecret }}
+  namespace: {{ include "release_namespace" . }}
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+type: kubernetes.io/dockerconfigjson
+data:
+  .dockerconfigjson: {{ template "secret" . }}
+{{- end }}
+{{- end }}
\ No newline at end of file
diff --git a/charts/longhorn-1.4.1/templates/serviceaccount.yaml b/charts/longhorn-1.4.1/templates/serviceaccount.yaml
new file mode 100644
index 0000000..a563d68
--- /dev/null
+++ b/charts/longhorn-1.4.1/templates/serviceaccount.yaml
@@ -0,0 +1,21 @@
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: longhorn-service-account
+  namespace: {{ include "release_namespace" . }}
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+  {{- with .Values.serviceAccount.annotations }}
+  annotations:
+    {{- toYaml . | nindent 4 }}
+  {{- end }}
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: longhorn-support-bundle
+  namespace: {{ include "release_namespace" . }}
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+  {{- with .Values.serviceAccount.annotations }}
+  annotations:
+    {{- toYaml . | nindent 4 }}
+  {{- end }}
\ No newline at end of file
diff --git a/charts/longhorn-1.4.1/templates/services.yaml b/charts/longhorn-1.4.1/templates/services.yaml
new file mode 100644
index 0000000..cd008db
--- /dev/null
+++ b/charts/longhorn-1.4.1/templates/services.yaml
@@ -0,0 +1,74 @@
+apiVersion: v1
+kind: Service
+metadata:
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+    app: longhorn-conversion-webhook
+  name: longhorn-conversion-webhook
+  namespace: {{ include "release_namespace" . }}
+spec:
+  type: ClusterIP
+  sessionAffinity: ClientIP
+  selector:
+    app: longhorn-conversion-webhook
+  ports:
+  - name: conversion-webhook
+    port: 9443
+    targetPort: conversion-wh
+---
+apiVersion: v1
+kind: Service
+metadata:
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+    app: longhorn-admission-webhook
+  name: longhorn-admission-webhook
+  namespace: {{ include "release_namespace" . }}
+spec:
+  type: ClusterIP
+  sessionAffinity: ClientIP
+  selector:
+    app: longhorn-admission-webhook
+  ports:
+  - name: admission-webhook
+    port: 9443
+    targetPort: admission-wh
+---
+apiVersion: v1
+kind: Service
+metadata:
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+    app: longhorn-recovery-backend
+  name: longhorn-recovery-backend
+  namespace: {{ include "release_namespace" . }}
+spec:
+  type: ClusterIP
+  sessionAffinity: ClientIP
+  selector:
+    app: longhorn-recovery-backend
+  ports:
+  - name: recovery-backend
+    port: 9600
+    targetPort: recov-backend
+---
+apiVersion: v1
+kind: Service
+metadata:
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+  name: longhorn-engine-manager
+  namespace: {{ include "release_namespace" . }}
+spec:
+  clusterIP: None
+  selector:
+    longhorn.io/component: instance-manager
+    longhorn.io/instance-manager-type: engine
+---
+apiVersion: v1
+kind: Service
+metadata:
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+  name: longhorn-replica-manager
+  namespace: {{ include "release_namespace" . }}
+spec:
+  clusterIP: None
+  selector:
+    longhorn.io/component: instance-manager
+    longhorn.io/instance-manager-type: replica
diff --git a/charts/longhorn-1.4.1/templates/storageclass.yaml b/charts/longhorn-1.4.1/templates/storageclass.yaml
new file mode 100644
index 0000000..6832517
--- /dev/null
+++ b/charts/longhorn-1.4.1/templates/storageclass.yaml
@@ -0,0 +1,44 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: longhorn-storageclass
+  namespace: {{ include "release_namespace" . }}
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+data:
+  storageclass.yaml: |
+    kind: StorageClass
+    apiVersion: storage.k8s.io/v1
+    metadata:
+      name: longhorn
+      annotations:
+        storageclass.kubernetes.io/is-default-class: {{ .Values.persistence.defaultClass | quote }}
+    provisioner: driver.longhorn.io
+    allowVolumeExpansion: true
+    reclaimPolicy: "{{ .Values.persistence.reclaimPolicy }}"
+    volumeBindingMode: Immediate
+    parameters:
+      numberOfReplicas: "{{ .Values.persistence.defaultClassReplicaCount }}"
+      staleReplicaTimeout: "30"
+      fromBackup: ""
+      {{- if .Values.persistence.defaultFsType }}
+      fsType: "{{ .Values.persistence.defaultFsType }}"
+      {{- end }}
+      {{- if .Values.persistence.defaultMkfsParams }}
+      mkfsParams: "{{ .Values.persistence.defaultMkfsParams }}"
+      {{- end }}
+      {{- if .Values.persistence.migratable }}
+      migratable: "{{ .Values.persistence.migratable }}"
+      {{- end }}    
+      {{- if .Values.persistence.backingImage.enable }}
+      backingImage: {{ .Values.persistence.backingImage.name }}
+      backingImageDataSourceType: {{ .Values.persistence.backingImage.dataSourceType }}
+      backingImageDataSourceParameters: {{ .Values.persistence.backingImage.dataSourceParameters }}
+      backingImageChecksum: {{ .Values.persistence.backingImage.expectedChecksum }}
+      {{- end }}
+      {{- if .Values.persistence.recurringJobSelector.enable }}
+      recurringJobSelector: '{{ .Values.persistence.recurringJobSelector.jobList }}'
+      {{- end }}
+      dataLocality: {{ .Values.persistence.defaultDataLocality | quote }}
+      {{- if .Values.persistence.defaultNodeSelector.enable }}
+      nodeSelector: "{{ .Values.persistence.defaultNodeSelector.selector }}"
+      {{- end }}
diff --git a/charts/longhorn-1.4.1/templates/tls-secrets.yaml b/charts/longhorn-1.4.1/templates/tls-secrets.yaml
new file mode 100644
index 0000000..74c4342
--- /dev/null
+++ b/charts/longhorn-1.4.1/templates/tls-secrets.yaml
@@ -0,0 +1,16 @@
+{{- if .Values.ingress.enabled }}
+{{- range .Values.ingress.secrets }}
+apiVersion: v1
+kind: Secret
+metadata:
+  name: {{ .name }}
+  namespace: {{ include "release_namespace" $ }}
+  labels: {{- include "longhorn.labels" $ | nindent 4 }}
+    app: longhorn
+type: kubernetes.io/tls
+data:
+  tls.crt: {{ .certificate | b64enc }}
+  tls.key: {{ .key | b64enc }}
+---
+{{- end }}
+{{- end }}
diff --git a/charts/longhorn-1.4.1/templates/uninstall-job.yaml b/charts/longhorn-1.4.1/templates/uninstall-job.yaml
new file mode 100644
index 0000000..989933d
--- /dev/null
+++ b/charts/longhorn-1.4.1/templates/uninstall-job.yaml
@@ -0,0 +1,59 @@
+apiVersion: batch/v1
+kind: Job
+metadata:
+  annotations:
+    "helm.sh/hook": pre-delete
+    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
+  name: longhorn-uninstall
+  namespace: {{ include "release_namespace" . }}
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+spec:
+  activeDeadlineSeconds: 900
+  backoffLimit: 1
+  template:
+    metadata:
+      name: longhorn-uninstall
+      labels: {{- include "longhorn.labels" . | nindent 8 }}
+    spec:
+      containers:
+      - name: longhorn-uninstall
+        image: {{ template "registry_url" . }}{{ .Values.image.longhorn.manager.repository }}:{{ .Values.image.longhorn.manager.tag }}
+        imagePullPolicy: {{ .Values.image.pullPolicy }}
+        securityContext:
+          privileged: true
+        command:
+        - longhorn-manager
+        - uninstall
+        - --force
+        env:
+        - name: LONGHORN_NAMESPACE
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.namespace
+      restartPolicy: Never
+      {{- if .Values.privateRegistry.registrySecret }}
+      imagePullSecrets:
+      - name: {{ .Values.privateRegistry.registrySecret }}
+      {{- end }}
+      {{- if .Values.longhornManager.priorityClass }}
+      priorityClassName: {{ .Values.longhornManager.priorityClass | quote }}
+      {{- end }}
+      serviceAccountName: longhorn-service-account
+      {{- if or .Values.longhornManager.tolerations .Values.global.cattle.windowsCluster.enabled }}
+      tolerations:
+        {{- if and .Values.global.cattle.windowsCluster.enabled .Values.global.cattle.windowsCluster.tolerations }}
+{{ toYaml .Values.global.cattle.windowsCluster.tolerations | indent 6 }}
+        {{- end }}
+        {{- if .Values.longhornManager.tolerations }}
+{{ toYaml .Values.longhornManager.tolerations | indent 6 }}
+        {{- end }}
+      {{- end }}
+      {{- if or .Values.longhornManager.nodeSelector .Values.global.cattle.windowsCluster.enabled }}
+      nodeSelector:
+        {{- if and .Values.global.cattle.windowsCluster.enabled .Values.global.cattle.windowsCluster.nodeSelector }}
+{{ toYaml .Values.global.cattle.windowsCluster.nodeSelector | indent 8 }}
+        {{- end }}
+        {{- if or .Values.longhornManager.nodeSelector }}
+{{ toYaml .Values.longhornManager.nodeSelector | indent 8 }}
+        {{- end }}
+      {{- end }}
diff --git a/charts/longhorn-1.4.1/templates/validate-psp-install.yaml b/charts/longhorn-1.4.1/templates/validate-psp-install.yaml
new file mode 100644
index 0000000..0df98e3
--- /dev/null
+++ b/charts/longhorn-1.4.1/templates/validate-psp-install.yaml
@@ -0,0 +1,7 @@
+#{{- if gt (len (lookup "rbac.authorization.k8s.io/v1" "ClusterRole" "" "")) 0 -}}
+#{{- if .Values.enablePSP }}
+#{{- if not (.Capabilities.APIVersions.Has "policy/v1beta1/PodSecurityPolicy") }}
+#{{- fail "The target cluster does not have the PodSecurityPolicy API resource. Please disable PSPs in this chart before proceeding." -}}
+#{{- end }}
+#{{- end }}
+#{{- end }}
\ No newline at end of file
diff --git a/charts/longhorn-1.4.1/values.yaml b/charts/longhorn-1.4.1/values.yaml
new file mode 100644
index 0000000..3ded6cd
--- /dev/null
+++ b/charts/longhorn-1.4.1/values.yaml
@@ -0,0 +1,332 @@
+# Default values for longhorn.
+# This is a YAML-formatted file.
+# Declare variables to be passed into your templates.
+global:
+  cattle:
+    systemDefaultRegistry: ""
+    windowsCluster:
+      # Enable this to allow Longhorn to run on the Rancher deployed Windows cluster
+      enabled: false
+      # Tolerate Linux node taint
+      tolerations:
+      - key: "cattle.io/os"
+        value: "linux"
+        effect: "NoSchedule"
+        operator: "Equal"
+      # Select Linux nodes
+      nodeSelector:
+        kubernetes.io/os: "linux"
+      # Recognize toleration and node selector for Longhorn run-time created components
+      defaultSetting:
+        taintToleration: cattle.io/os=linux:NoSchedule
+        systemManagedComponentsNodeSelector: kubernetes.io/os:linux
+
+image:
+  longhorn:
+    engine:
+      repository: longhornio/longhorn-engine
+      tag: v1.4.1
+    manager:
+      repository: longhornio/longhorn-manager
+      tag: v1.4.1
+    ui:
+      repository: longhornio/longhorn-ui
+      tag: v1.4.1
+    instanceManager:
+      repository: longhornio/longhorn-instance-manager
+      tag: v1.4.1
+    shareManager:
+      repository: longhornio/longhorn-share-manager
+      tag: v1.4.1
+    backingImageManager:
+      repository: longhornio/backing-image-manager
+      tag: v1.4.1
+    supportBundleKit:
+      repository: longhornio/support-bundle-kit
+      tag: v0.0.19
+  csi:
+    attacher:
+      repository: longhornio/csi-attacher
+      tag: v3.4.0
+    provisioner:
+      repository: longhornio/csi-provisioner
+      tag: v2.1.2
+    nodeDriverRegistrar:
+      repository: longhornio/csi-node-driver-registrar
+      tag: v2.5.0
+    resizer:
+      repository: longhornio/csi-resizer
+      tag: v1.3.0
+    snapshotter:
+      repository: longhornio/csi-snapshotter
+      tag: v5.0.1
+    livenessProbe:
+      repository: longhornio/livenessprobe
+      tag: v2.8.0
+  pullPolicy: IfNotPresent
+
+service:
+  ui:
+    type: ClusterIP
+    nodePort: null
+  manager:
+    type: ClusterIP
+    nodePort: ""
+    loadBalancerIP: ""
+    loadBalancerSourceRanges: ""
+
+persistence:
+  defaultClass: true
+  defaultFsType: ext4
+  defaultMkfsParams: ""
+  defaultClassReplicaCount: 3
+  defaultDataLocality: disabled # best-effort otherwise
+  reclaimPolicy: Delete
+  migratable: false
+  recurringJobSelector:
+    enable: false
+    jobList: []
+  backingImage:
+    enable: false
+    name: ~
+    dataSourceType: ~
+    dataSourceParameters: ~
+    expectedChecksum: ~
+  defaultNodeSelector:
+    enable: false # disable by default
+    selector: []
+  removeSnapshotsDuringFilesystemTrim: ignored # "enabled" or "disabled" otherwise
+
+csi:
+  kubeletRootDir: ~
+  attacherReplicaCount: ~
+  provisionerReplicaCount: ~
+  resizerReplicaCount: ~
+  snapshotterReplicaCount: ~
+
+defaultSettings:
+  backupTarget: ~
+  backupTargetCredentialSecret: ~
+  allowRecurringJobWhileVolumeDetached: ~
+  createDefaultDiskLabeledNodes: ~
+  defaultDataPath: ~
+  defaultDataLocality: ~
+  replicaSoftAntiAffinity: ~
+  replicaAutoBalance: ~
+  storageOverProvisioningPercentage: ~
+  storageMinimalAvailablePercentage: ~
+  upgradeChecker: ~
+  defaultReplicaCount: ~
+  defaultLonghornStaticStorageClass: ~
+  backupstorePollInterval: ~
+  failedBackupTTL: ~
+  restoreVolumeRecurringJobs: ~
+  recurringSuccessfulJobsHistoryLimit: ~
+  recurringFailedJobsHistoryLimit: ~
+  supportBundleFailedHistoryLimit: ~
+  taintToleration: ~
+  systemManagedComponentsNodeSelector: ~
+  priorityClass: ~
+  autoSalvage: ~
+  autoDeletePodWhenVolumeDetachedUnexpectedly: ~
+  disableSchedulingOnCordonedNode: ~
+  replicaZoneSoftAntiAffinity: ~
+  nodeDownPodDeletionPolicy: ~
+  allowNodeDrainWithLastHealthyReplica: ~
+  mkfsExt4Parameters: ~
+  disableReplicaRebuild: ~
+  replicaReplenishmentWaitInterval: ~
+  concurrentReplicaRebuildPerNodeLimit: ~
+  concurrentVolumeBackupRestorePerNodeLimit: ~
+  disableRevisionCounter: ~
+  systemManagedPodsImagePullPolicy: ~
+  allowVolumeCreationWithDegradedAvailability: ~
+  autoCleanupSystemGeneratedSnapshot: ~
+  concurrentAutomaticEngineUpgradePerNodeLimit: ~
+  backingImageCleanupWaitInterval: ~
+  backingImageRecoveryWaitInterval: ~
+  guaranteedEngineManagerCPU: ~
+  guaranteedReplicaManagerCPU: ~
+  kubernetesClusterAutoscalerEnabled: ~
+  orphanAutoDeletion: ~
+  storageNetwork: ~
+  deletingConfirmationFlag: ~
+  engineReplicaTimeout: ~
+  snapshotDataIntegrity: ~
+  snapshotDataIntegrityImmediateCheckAfterSnapshotCreation: ~
+  snapshotDataIntegrityCronjob: ~
+  removeSnapshotsDuringFilesystemTrim: ~
+  fastReplicaRebuildEnabled: ~
+  replicaFileSyncHttpClientTimeout: ~
+privateRegistry:
+  createSecret: ~
+  registryUrl: ~
+  registryUser: ~
+  registryPasswd: ~
+  registrySecret: ~
+
+longhornManager:
+  log:
+    ## Allowed values are `plain` or `json`.
+    format: plain
+  priorityClass: ~
+  tolerations: []
+  ## If you want to set tolerations for Longhorn Manager DaemonSet, delete the `[]` in the line above
+  ## and uncomment this example block
+  # - key: "key"
+  #   operator: "Equal"
+  #   value: "value"
+  #   effect: "NoSchedule"
+  nodeSelector: {}
+  ## If you want to set node selector for Longhorn Manager DaemonSet, delete the `{}` in the line above
+  ## and uncomment this example block
+  #  label-key1: "label-value1"
+  #  label-key2: "label-value2"
+  serviceAnnotations: {}
+  ## If you want to set annotations for the Longhorn Manager service, delete the `{}` in the line above
+  ## and uncomment this example block
+  #  annotation-key1: "annotation-value1"
+  #  annotation-key2: "annotation-value2"
+
+longhornDriver:
+  priorityClass: ~
+  tolerations: []
+  ## If you want to set tolerations for Longhorn Driver Deployer Deployment, delete the `[]` in the line above
+  ## and uncomment this example block
+  # - key: "key"
+  #   operator: "Equal"
+  #   value: "value"
+  #   effect: "NoSchedule"
+  nodeSelector: {}
+  ## If you want to set node selector for Longhorn Driver Deployer Deployment, delete the `{}` in the line above
+  ## and uncomment this example block
+  #  label-key1: "label-value1"
+  #  label-key2: "label-value2"
+
+longhornUI:
+  replicas: 2
+  priorityClass: ~
+  tolerations: []
+  ## If you want to set tolerations for Longhorn UI Deployment, delete the `[]` in the line above
+  ## and uncomment this example block
+  # - key: "key"
+  #   operator: "Equal"
+  #   value: "value"
+  #   effect: "NoSchedule"
+  nodeSelector: {}
+  ## If you want to set node selector for Longhorn UI Deployment, delete the `{}` in the line above
+  ## and uncomment this example block
+  #  label-key1: "label-value1"
+  #  label-key2: "label-value2"
+
+longhornConversionWebhook:
+  replicas: 2
+  priorityClass: ~
+  tolerations: []
+  ## If you want to set tolerations for Longhorn conversion webhook Deployment, delete the `[]` in the line above
+  ## and uncomment this example block
+  # - key: "key"
+  #   operator: "Equal"
+  #   value: "value"
+  #   effect: "NoSchedule"
+  nodeSelector: {}
+  ## If you want to set node selector for Longhorn conversion webhook Deployment, delete the `{}` in the line above
+  ## and uncomment this example block
+  #  label-key1: "label-value1"
+  #  label-key2: "label-value2"
+
+longhornAdmissionWebhook:
+  replicas: 2
+  priorityClass: ~
+  tolerations: []
+  ## If you want to set tolerations for Longhorn admission webhook Deployment, delete the `[]` in the line above
+  ## and uncomment this example block
+  # - key: "key"
+  #   operator: "Equal"
+  #   value: "value"
+  #   effect: "NoSchedule"
+  nodeSelector: {}
+  ## If you want to set node selector for Longhorn admission webhook Deployment, delete the `{}` in the line above
+  ## and uncomment this example block
+  #  label-key1: "label-value1"
+  #  label-key2: "label-value2"
+
+longhornRecoveryBackend:
+  replicas: 2
+  priorityClass: ~
+  tolerations: []
+  ## If you want to set tolerations for Longhorn recovery backend Deployment, delete the `[]` in the line above
+  ## and uncomment this example block
+  # - key: "key"
+  #   operator: "Equal"
+  #   value: "value"
+  #   effect: "NoSchedule"
+  nodeSelector: {}
+  ## If you want to set node selector for Longhorn recovery backend Deployment, delete the `{}` in the line above
+  ## and uncomment this example block
+  #  label-key1: "label-value1"
+  #  label-key2: "label-value2"
+
+ingress:
+  ## Set to true to enable ingress record generation
+  enabled: false
+
+  ## Add ingressClassName to the Ingress
+  ## Can replace the kubernetes.io/ingress.class annotation on v1.18+
+  ingressClassName: ~
+
+  host: sslip.io
+
+  ## Set this to true in order to enable TLS on the ingress record
+  tls: false
+
+  ## Enable this in order to enable that the backend service will be connected at port 443
+  secureBackends: false
+
+  ## If TLS is set to true, you must declare what secret will store the key/certificate for TLS
+  tlsSecret: longhorn.local-tls
+
+  ## If ingress is enabled you can set the default ingress path
+  ## then you can access the UI by using the following full path {{host}}+{{path}}
+  path: /
+
+  ## Ingress annotations done as key:value pairs
+  ## If you're using kube-lego, you will want to add:
+  ## kubernetes.io/tls-acme: true
+  ##
+  ## For a full list of possible ingress annotations, please see
+  ## ref: https://github.com/kubernetes/ingress-nginx/blob/master/docs/annotations.md
+  ##
+  ## If tls is set to true, annotation ingress.kubernetes.io/secure-backends: "true" will automatically be set
+  annotations:
+  #  kubernetes.io/ingress.class: nginx
+  #  kubernetes.io/tls-acme: true
+
+  secrets:
+  ## If you're providing your own certificates, please use this to add the certificates as secrets
+  ## key and certificate should start with -----BEGIN CERTIFICATE----- or
+  ## -----BEGIN RSA PRIVATE KEY-----
+  ##
+  ## name should line up with a tlsSecret set further up
+  ## If you're using kube-lego, this is unneeded, as it will create the secret for you if it is not set
+  ##
+  ## It is also possible to create and manage the certificates outside of this helm chart
+  ## Please see README.md for more information
+  # - name: longhorn.local-tls
+  #   key:
+  #   certificate:
+
+#  For Kubernetes < v1.25, if your cluster enables Pod Security Policy admission controller,
+#  set this to `true` to ship longhorn-psp which allow privileged Longhorn pods to start
+enablePSP: false
+
+## Specify override namespace, specifically this is useful for using longhorn as sub-chart
+## and its release namespace is not the `longhorn-system`
+namespaceOverride: ""
+
+# Annotations to add to the Longhorn Manager DaemonSet Pods. Optional.
+annotations: {}
+
+serviceAccount:
+  # Annotations to add to the service account
+  annotations: {}
diff --git a/charts/longhorn/Chart.yaml b/charts/longhorn/Chart.yaml
index d9dd5a1..1609eb5 100644
--- a/charts/longhorn/Chart.yaml
+++ b/charts/longhorn/Chart.yaml
@@ -1,5 +1,5 @@
 apiVersion: v1
-appVersion: v1.4.1
+appVersion: v1.5.2
 description: Longhorn is a distributed block storage system for Kubernetes.
 home: https://github.com/longhorn/longhorn
 icon: https://raw.githubusercontent.com/cncf/artwork/master/projects/longhorn/icon/color/longhorn-icon-color.png
@@ -25,4 +25,4 @@
 - https://github.com/longhorn/longhorn-ui
 - https://github.com/longhorn/longhorn-tests
 - https://github.com/longhorn/backing-image-manager
-version: 1.4.1
+version: 1.5.2
diff --git a/charts/longhorn/questions.yaml b/charts/longhorn/questions.yaml
index b4ae9de..b53b0fe 100644
--- a/charts/longhorn/questions.yaml
+++ b/charts/longhorn/questions.yaml
@@ -17,7 +17,7 @@
     label: Longhorn Manager Image Repository
     group: "Longhorn Images Settings"
   - variable: image.longhorn.manager.tag
-    default: v1.4.1
+    default: v1.5.2
     description: "Specify Longhorn Manager Image Tag"
     type: string
     label: Longhorn Manager Image Tag
@@ -29,7 +29,7 @@
     label: Longhorn Engine Image Repository
     group: "Longhorn Images Settings"
   - variable: image.longhorn.engine.tag
-    default: v1.4.1
+    default: v1.5.2
     description: "Specify Longhorn Engine Image Tag"
     type: string
     label: Longhorn Engine Image Tag
@@ -41,7 +41,7 @@
     label: Longhorn UI Image Repository
     group: "Longhorn Images Settings"
   - variable: image.longhorn.ui.tag
-    default: v1.4.1
+    default: v1.5.2
     description: "Specify Longhorn UI Image Tag"
     type: string
     label: Longhorn UI Image Tag
@@ -53,7 +53,7 @@
     label: Longhorn Instance Manager Image Repository
     group: "Longhorn Images Settings"
   - variable: image.longhorn.instanceManager.tag
-    default: v1.4.1
+    default: v1.5.2
     description: "Specify Longhorn Instance Manager Image Tag"
     type: string
     label: Longhorn Instance Manager Image Tag
@@ -65,7 +65,7 @@
     label: Longhorn Share Manager Image Repository
     group: "Longhorn Images Settings"
   - variable: image.longhorn.shareManager.tag
-    default: v1.4.1
+    default: v1.5.2
     description: "Specify Longhorn Share Manager Image Tag"
     type: string
     label: Longhorn Share Manager Image Tag
@@ -77,7 +77,7 @@
     label: Longhorn Backing Image Manager Image Repository
     group: "Longhorn Images Settings"
   - variable: image.longhorn.backingImageManager.tag
-    default: v1.4.1
+    default: v1.5.2
     description: "Specify Longhorn Backing Image Manager Image Tag"
     type: string
     label: Longhorn Backing Image Manager Image Tag
@@ -89,7 +89,7 @@
     label: Longhorn Support Bundle Kit Image Repository
     group: "Longhorn Images Settings"
   - variable: image.longhorn.supportBundleKit.tag
-    default: v0.0.17
+    default: v0.0.27
     description: "Specify Longhorn Support Bundle Manager Image Tag"
     type: string
     label: Longhorn Support Bundle Kit Image Tag
@@ -101,7 +101,7 @@
     label: Longhorn CSI Attacher Image Repository
     group: "Longhorn CSI Driver Images"
   - variable: image.csi.attacher.tag
-    default: v3.4.0
+    default: v4.2.0
     description: "Specify CSI attacher image tag. Leave blank to autodetect."
     type: string
     label: Longhorn CSI Attacher Image Tag
@@ -113,7 +113,7 @@
     label: Longhorn CSI Provisioner Image Repository
     group: "Longhorn CSI Driver Images"
   - variable: image.csi.provisioner.tag
-    default: v2.1.2
+    default: v3.4.1
     description: "Specify CSI provisioner image tag. Leave blank to autodetect."
     type: string
     label: Longhorn CSI Provisioner Image Tag
@@ -125,7 +125,7 @@
     label: Longhorn CSI Node Driver Registrar Image Repository
     group: "Longhorn CSI Driver Images"
   - variable: image.csi.nodeDriverRegistrar.tag
-    default: v2.5.0
+    default: v2.7.0
     description: "Specify CSI Node Driver Registrar image tag. Leave blank to autodetect."
     type: string
     label: Longhorn CSI Node Driver Registrar Image Tag
@@ -137,7 +137,7 @@
     label: Longhorn CSI Driver Resizer Image Repository
     group: "Longhorn CSI Driver Images"
   - variable: image.csi.resizer.tag
-    default: v1.3.0
+    default: v1.7.0
     description: "Specify CSI Driver Resizer image tag. Leave blank to autodetect."
     type: string
     label: Longhorn CSI Driver Resizer Image Tag
@@ -149,7 +149,7 @@
     label: Longhorn CSI Driver Snapshotter Image Repository
     group: "Longhorn CSI Driver Images"
   - variable: image.csi.snapshotter.tag
-    default: v5.0.1
+    default: v6.2.1
     description: "Specify CSI Driver Snapshotter image tag. Leave blank to autodetect."
     type: string
     label: Longhorn CSI Driver Snapshotter Image Tag
@@ -161,7 +161,7 @@
     label: Longhorn CSI Liveness Probe Image Repository
     group: "Longhorn CSI Driver Images"
   - variable: image.csi.livenessProbe.tag
-    default: v2.8.0
+    default: v2.9.0
     description: "Specify CSI liveness probe image tag. Leave blank to autodetect."
     type: string
     label: Longhorn CSI Liveness Probe Image Tag
@@ -327,6 +327,14 @@
     min: 0
     max: 100
     default: 25
+  - variable: defaultSettings.storageReservedPercentageForDefaultDisk
+    label: Storage Reserved Percentage For Default Disk
+    description: "The reserved percentage specifies the percentage of disk space that will not be allocated to the default disk on each new Longhorn node."
+    group: "Longhorn Default Settings"
+    type: int
+    min: 0
+    max: 100
+    default: 30
   - variable: defaultSettings.upgradeChecker
     label: Enable Upgrade Checker
     description: 'Upgrade Checker will check for new Longhorn version periodically. When there is a new version available, a notification will appear in the UI. By default true.'
@@ -439,24 +447,19 @@
     - "delete-deployment-pod"
     - "delete-both-statefulset-and-deployment-pod"
     default: "do-nothing"
-  - variable: defaultSettings.allowNodeDrainWithLastHealthyReplica
-    label: Allow Node Drain with the Last Healthy Replica
-    description: "By default, Longhorn will block `kubectl drain` action on a node if the node contains the last healthy replica of a volume.
-If this setting is enabled, Longhorn will **not** block `kubectl drain` action on a node even if the node contains the last healthy replica of a volume."
+  - variable: defaultSettings.nodeDrainPolicy
+    label: Node Drain Policy
+    description: "Define the policy to use when a node with the last healthy replica of a volume is drained.
+- **block-if-contains-last-replica** Longhorn will block the drain when the node contains the last healthy replica of a volume.
+- **allow-if-replica-is-stopped** Longhorn will allow the drain when the node contains the last healthy replica of a volume but the replica is stopped. WARNING: possible data loss if the node is removed after draining. Select this option if you want to drain the node and do in-place upgrade/maintenance.
+- **always-allow** Longhorn will allow the drain even though the node contains the last healthy replica of a volume. WARNING: possible data loss if the node is removed after draining. Also possible data corruption if the last replica was running during the draining."
     group: "Longhorn Default Settings"
-    type: boolean
-    default: "false"
-  - variable: defaultSettings.mkfsExt4Parameters
-    label: Custom mkfs.ext4 parameters
-    description: "Allows setting additional filesystem creation parameters for ext4. For older host kernels it might be necessary to disable the optional ext4 metadata_csum feature by specifying `-O ^64bit,^metadata_csum`."
-    group: "Longhorn Default Settings"
-    type: string
-  - variable: defaultSettings.disableReplicaRebuild
-    label: Disable Replica Rebuild
-    description: "This setting disable replica rebuild cross the whole cluster, eviction and data locality feature won't work if this setting is true. But doesn't have any impact to any current replica rebuild and restore disaster recovery volume."
-    group: "Longhorn Default Settings"
-    type: boolean
-    default: "false"
+    type: enum
+    options:
+      - "block-if-contains-last-replica"
+      - "allow-if-replica-is-stopped"
+      - "always-allow"
+    default: "block-if-contains-last-replica"
   - variable: defaultSettings.replicaReplenishmentWaitInterval
     label: Replica Replenishment Wait Interval
     description: "In seconds. The interval determines how long Longhorn will wait at least in order to reuse the existing data on a failed replica rather than directly creating a new replica for a degraded volume.
@@ -538,42 +541,30 @@
     type: int
     min: 0
     default: 300
-  - variable: defaultSettings.guaranteedEngineManagerCPU
-    label: Guaranteed Engine Manager CPU
-    description: "This integer value indicates how many percentage of the total allocatable CPU on each node will be reserved for each engine manager Pod. For example, 10 means 10% of the total CPU on a node will be allocated to each engine manager pod on this node. This will help maintain engine stability during high node workload.
-    In order to prevent unexpected volume engine crash as well as guarantee a relative acceptable IO performance, you can use the following formula to calculate a value for this setting:
-    Guaranteed Engine Manager CPU = The estimated max Longhorn volume engine count on a node * 0.1 / The total allocatable CPUs on the node * 100.
+  - variable: defaultSettings.guaranteedInstanceManagerCPU
+    label: Guaranteed Instance Manager CPU
+    description: "This integer value indicates how many percentage of the total allocatable CPU on each node will be reserved for each instance manager Pod. For example, 10 means 10% of the total CPU on a node will be allocated to each instance manager pod on this node. This will help maintain engine and replica stability during high node workload.
+    In order to prevent unexpected volume instance (engine/replica) crash as well as guarantee a relative acceptable IO performance, you can use the following formula to calculate a value for this setting:
+    `Guaranteed Instance Manager CPU = The estimated max Longhorn volume engine and replica count on a node * 0.1 / The total allocatable CPUs on the node * 100`
     The result of above calculation doesn't mean that's the maximum CPU resources the Longhorn workloads require. To fully exploit the Longhorn volume I/O performance, you can allocate/guarantee more CPU resources via this setting.
     If it's hard to estimate the usage now, you can leave it with the default value, which is 12%. Then you can tune it when there is no running workload using Longhorn volumes.
     WARNING:
-      - Value 0 means unsetting CPU requests for engine manager pods.
-      - Considering the possible new instance manager pods in the further system upgrade, this integer value is range from 0 to 40. And the sum with setting 'Guaranteed Engine Manager CPU' should not be greater than 40.
+      - Value 0 means unsetting CPU requests for instance manager pods.
+      - Considering the possible new instance manager pods in the further system upgrade, this integer value is range from 0 to 40. 
       - One more set of instance manager pods may need to be deployed when the Longhorn system is upgraded. If current available CPUs of the nodes are not enough for the new instance manager pods, you need to detach the volumes using the oldest instance manager pods so that Longhorn can clean up the old pods automatically and release the CPU resources. And the new pods with the latest instance manager image will be launched then.
-      - This global setting will be ignored for a node if the field \"EngineManagerCPURequest\" on the node is set.
-      - After this setting is changed, all engine manager pods using this global setting on all the nodes will be automatically restarted. In other words, DO NOT CHANGE THIS SETTING WITH ATTACHED VOLUMES."
+      - This global setting will be ignored for a node if the field \"InstanceManagerCPURequest\" on the node is set.
+      - After this setting is changed, all instance manager pods using this global setting on all the nodes will be automatically restarted. In other words, DO NOT CHANGE THIS SETTING WITH ATTACHED VOLUMES."
     group: "Longhorn Default Settings"
     type: int
     min: 0
     max: 40
     default: 12
-  - variable: defaultSettings.guaranteedReplicaManagerCPU
-    label: Guaranteed Replica Manager CPU
-    description: "This integer value indicates how many percentage of the total allocatable CPU on each node will be reserved for each replica manager Pod. 10 means 10% of the total CPU on a node will be allocated to each replica manager pod on this node. This will help maintain replica stability during high node workload.
-    In order to prevent unexpected volume replica crash as well as guarantee a relative acceptable IO performance, you can use the following formula to calculate a value for this setting:
-    Guaranteed Replica Manager CPU = The estimated max Longhorn volume replica count on a node * 0.1 / The total allocatable CPUs on the node * 100.
-    The result of above calculation doesn't mean that's the maximum CPU resources the Longhorn workloads require. To fully exploit the Longhorn volume I/O performance, you can allocate/guarantee more CPU resources via this setting.
-    If it's hard to estimate the usage now, you can leave it with the default value, which is 12%. Then you can tune it when there is no running workload using Longhorn volumes.
-    WARNING:
-      - Value 0 means unsetting CPU requests for replica manager pods.
-      - Considering the possible new instance manager pods in the further system upgrade, this integer value is range from 0 to 40. And the sum with setting 'Guaranteed Replica Manager CPU' should not be greater than 40.
-      - One more set of instance manager pods may need to be deployed when the Longhorn system is upgraded. If current available CPUs of the nodes are not enough for the new instance manager pods, you need to detach the volumes using the oldest instance manager pods so that Longhorn can clean up the old pods automatically and release the CPU resources. And the new pods with the latest instance manager image will be launched then.
-      - This global setting will be ignored for a node if the field \"ReplicaManagerCPURequest\" on the node is set.
-      - After this setting is changed, all replica manager pods using this global setting on all the nodes will be automatically restarted. In other words, DO NOT CHANGE THIS SETTING WITH ATTACHED VOLUMES."
+  - variable: defaultSettings.logLevel
+    label: Log Level
+    description: "The log level Panic, Fatal, Error, Warn, Info, Debug, Trace used in longhorn manager. By default Debug."
     group: "Longhorn Default Settings"
-    type: int
-    min: 0
-    max: 40
-    default: 12
+    type: string
+    default: "Info"
 - variable: defaultSettings.kubernetesClusterAutoscalerEnabled
   label: Kubernetes Cluster Autoscaler Enabled (Experimental)
   description: "Enabling this setting will notify Longhorn that the cluster is using Kubernetes Cluster Autoscaler.
@@ -660,6 +651,50 @@
   group: "Longhorn Default Settings"
   type: int
   default: "30"
+- variable: defaultSettings.backupCompressionMethod
+  label: Backup Compression Method
+  description: "This setting allows users to specify backup compression method.
+  Available options are
+    - **none**: Disable the compression method. Suitable for multimedia data such as encoded images and videos.
+    - **lz4**: Fast compression method. Suitable for flat files.
+    - **gzip**: A bit of higher compression ratio but relatively slow."
+  group: "Longhorn Default Settings"
+  type: string
+  default: "lz4"
+- variable: defaultSettings.backupConcurrentLimit
+  label: Backup Concurrent Limit Per Backup
+  description: "This setting controls how many worker threads per backup concurrently."
+  group: "Longhorn Default Settings"
+  type: int
+  min: 1
+  default: 2
+- variable: defaultSettings.restoreConcurrentLimit
+  label: Restore Concurrent Limit Per Backup
+  description: "This setting controls how many worker threads per restore concurrently."
+  group: "Longhorn Default Settings"
+  type: int
+  min: 1
+  default: 2
+- variable: defaultSettings.v2DataEngine
+  label: V2 Data Engine
+  description: "This allows users to activate v2 data engine based on SPDK. Currently, it is in the preview phase and should not be utilized in a production environment.
+	WARNING:
+	  - DO NOT CHANGE THIS SETTING WITH ATTACHED VOLUMES. Longhorn will block this setting update when there are attached volumes.
+	  - When applying the setting, Longhorn will restart all instance-manager pods.
+	  - When the V2 Data Engine is enabled, each instance-manager pod utilizes 1 CPU core. This high CPU usage is attributed to the spdk_tgt process running within each instance-manager pod. The spdk_tgt process is responsible for handling input/output (IO) operations and requires intensive polling. As a result, it consumes 100% of a dedicated CPU core to efficiently manage and process the IO requests, ensuring optimal performance and responsiveness for storage operations."
+  group: "Longhorn V2 Data Engine (Preview Feature) Settings"
+  type: boolean
+  default: false
+- variable: defaultSettings.offlineReplicaRebuilding
+  label: Offline Replica Rebuilding
+  description: ""This setting allows users to enable the offline replica rebuilding for volumes using v2 data engine."
+  group: "Longhorn V2 Data Engine (Preview Feature) Settings"
+  required: true
+  type: enum
+  options:
+  - "enabled"
+  - "disabled"
+  default: "enabled"
 - variable: persistence.defaultClass
   default: "true"
   description: "Set as default StorageClass for Longhorn"
@@ -708,18 +743,18 @@
     group: "Longhorn Storage Class Settings"
     type: string
     default:
-- variable: defaultSettings.defaultNodeSelector.enable
-  description: "Enable recurring Node selector for Longhorn StorageClass"
+- variable: persistence.defaultNodeSelector.enable
+  description: "Enable Node selector for Longhorn StorageClass"
   group: "Longhorn Storage Class Settings"
   label: Enable Storage Class Node Selector
   type: boolean
   default: false
   show_subquestion_if: true
   subquestions:
-  - variable: defaultSettings.defaultNodeSelector.selector
+  - variable: persistence.defaultNodeSelector.selector
     label: Storage Class Node Selector
-    description: 'We use NodeSelector when we want to bind PVC via StorageClass into desired mountpoint on the nodes tagged whith its value'
-    group: "Longhorn Default Settings"
+    description: 'We use NodeSelector when we want to bind PVC via StorageClass into desired mountpoint on the nodes tagged with its value'
+    group: "Longhorn Storage Class Settings"
     type: string
     default:
 - variable: persistence.backingImage.enable
@@ -835,3 +870,21 @@
   label: Rancher Windows Cluster
   type: boolean
   group: "Other Settings"
+- variable: networkPolicies.enabled
+  description: "Enable NetworkPolicies to limit access to the longhorn pods.
+  Warning: The Rancher Proxy will not work if this feature is enabled and a custom NetworkPolicy must be added."
+  group: "Other Settings"
+  label: Network Policies
+  default: "false"
+  type: boolean
+  subquestions:
+  - variable: networkPolicies.type
+    label: Network Policies for Ingress
+    description: "Create the policy to allow access for the ingress, select the distribution."
+    show_if: "networkPolicies.enabled=true&&ingress.enabled=true"
+    type: enum
+    default: "rke2"
+    options:
+      - "rke1"
+      - "rke2"
+      - "k3s"
diff --git a/charts/longhorn/templates/clusterrole.yaml b/charts/longhorn/templates/clusterrole.yaml
index bf28a47..e652a34 100644
--- a/charts/longhorn/templates/clusterrole.yaml
+++ b/charts/longhorn/templates/clusterrole.yaml
@@ -41,7 +41,8 @@
               "backingimagemanagers", "backingimagemanagers/status", "backingimagedatasources", "backingimagedatasources/status",
               "backuptargets", "backuptargets/status", "backupvolumes", "backupvolumes/status", "backups", "backups/status",
               "recurringjobs", "recurringjobs/status", "orphans", "orphans/status", "snapshots", "snapshots/status",
-              "supportbundles", "supportbundles/status", "systembackups", "systembackups/status", "systemrestores", "systemrestores/status"]
+              "supportbundles", "supportbundles/status", "systembackups", "systembackups/status", "systemrestores", "systemrestores/status",
+              "volumeattachments", "volumeattachments/status"]
   verbs: ["*"]
 - apiGroups: ["coordination.k8s.io"]
   resources: ["leases"]
diff --git a/charts/longhorn/templates/crds.yaml b/charts/longhorn/templates/crds.yaml
index 0f73824..ac56efe 100644
--- a/charts/longhorn/templates/crds.yaml
+++ b/charts/longhorn/templates/crds.yaml
@@ -296,12 +296,6 @@
                   properties:
                     currentChecksum:
                       type: string
-                    directory:
-                      description: 'Deprecated: This field is useless.'
-                      type: string
-                    downloadProgress:
-                      description: 'Deprecated: This field is renamed to `Progress`.'
-                      type: integer
                     message:
                       type: string
                     name:
@@ -317,9 +311,6 @@
                       type: integer
                     state:
                       type: string
-                    url:
-                      description: 'Deprecated: This field is useless now. The manager of backing image files doesn''t care if a file is downloaded and how.'
-                      type: string
                     uuid:
                       type: string
                   type: object
@@ -364,7 +355,7 @@
           name: longhorn-conversion-webhook
           namespace: {{ include "release_namespace" . }}
           path: /v1/webhook/conversion
-          port: 9443
+          port: 9501
       conversionReviewVersions:
       - v1beta2
       - v1beta1
@@ -446,9 +437,6 @@
                 additionalProperties:
                   type: string
                 type: object
-              imageURL:
-                description: 'Deprecated: This kind of info will be included in the related BackingImageDataSource.'
-                type: string
               sourceParameters:
                 additionalProperties:
                   type: string
@@ -465,19 +453,6 @@
             properties:
               checksum:
                 type: string
-              diskDownloadProgressMap:
-                additionalProperties:
-                  type: integer
-                description: 'Deprecated: Replaced by field `Progress` in `DiskFileStatusMap`.'
-                nullable: true
-                type: object
-              diskDownloadStateMap:
-                additionalProperties:
-                  description: BackingImageDownloadState is replaced by BackingImageState.
-                  type: string
-                description: 'Deprecated: Replaced by field `State` in `DiskFileStatusMap`.'
-                nullable: true
-                type: object
               diskFileStatusMap:
                 additionalProperties:
                   properties:
@@ -637,6 +612,9 @@
               backupCreatedAt:
                 description: The snapshot backup upload finished time.
                 type: string
+              compressionMethod:
+                description: Compression method
+                type: string
               error:
                 description: The error message when taking the snapshot backup.
                 type: string
@@ -724,7 +702,7 @@
           name: longhorn-conversion-webhook
           namespace: {{ include "release_namespace" . }}
           path: /v1/webhook/conversion
-          port: 9443
+          port: 9501
       conversionReviewVersions:
       - v1beta2
       - v1beta1
@@ -1032,6 +1010,9 @@
               size:
                 description: The backup volume size.
                 type: string
+              storageClassName:
+                description: the storage class name of pv/pvc binding with the volume.
+                type: string
             type: object
         type: object
     served: true
@@ -1064,7 +1045,7 @@
           name: longhorn-conversion-webhook
           namespace: {{ include "release_namespace" . }}
           path: /v1/webhook/conversion
-          port: 9443
+          port: 9501
       conversionReviewVersions:
       - v1beta2
       - v1beta1
@@ -1333,6 +1314,11 @@
             properties:
               active:
                 type: boolean
+              backendStoreDriver:
+                enum:
+                - v1
+                - v2
+                type: string
               backupVolume:
                 type: string
               desireState:
@@ -1345,6 +1331,7 @@
                 enum:
                 - blockdev
                 - iscsi
+                - nvmf
                 - ""
                 type: string
               logRequested:
@@ -1668,15 +1655,13 @@
           spec:
             description: InstanceManagerSpec defines the desired state of the Longhorn instancer manager
             properties:
-              engineImage:
-                description: 'Deprecated: This field is useless.'
-                type: string
               image:
                 type: string
               nodeID:
                 type: string
               type:
                 enum:
+                - aio
                 - engine
                 - replica
                 type: string
@@ -1694,11 +1679,13 @@
                 type: integer
               currentState:
                 type: string
-              instances:
+              instanceEngines:
                 additionalProperties:
                   properties:
                     spec:
                       properties:
+                        backendStoreDriver:
+                          type: string
                         name:
                           type: string
                       type: object
@@ -1727,6 +1714,77 @@
                   type: object
                 nullable: true
                 type: object
+              instanceReplicas:
+                additionalProperties:
+                  properties:
+                    spec:
+                      properties:
+                        backendStoreDriver:
+                          type: string
+                        name:
+                          type: string
+                      type: object
+                    status:
+                      properties:
+                        endpoint:
+                          type: string
+                        errorMsg:
+                          type: string
+                        listen:
+                          type: string
+                        portEnd:
+                          format: int32
+                          type: integer
+                        portStart:
+                          format: int32
+                          type: integer
+                        resourceVersion:
+                          format: int64
+                          type: integer
+                        state:
+                          type: string
+                        type:
+                          type: string
+                      type: object
+                  type: object
+                nullable: true
+                type: object
+              instances:
+                additionalProperties:
+                  properties:
+                    spec:
+                      properties:
+                        backendStoreDriver:
+                          type: string
+                        name:
+                          type: string
+                      type: object
+                    status:
+                      properties:
+                        endpoint:
+                          type: string
+                        errorMsg:
+                          type: string
+                        listen:
+                          type: string
+                        portEnd:
+                          format: int32
+                          type: integer
+                        portStart:
+                          format: int32
+                          type: integer
+                        resourceVersion:
+                          format: int64
+                          type: integer
+                        state:
+                          type: string
+                        type:
+                          type: string
+                      type: object
+                  type: object
+                nullable: true
+                description: 'Deprecated: Replaced by InstanceEngines and InstanceReplicas'
+                type: object
               ip:
                 type: string
               ownerID:
@@ -1763,7 +1821,7 @@
           name: longhorn-conversion-webhook
           namespace: {{ include "release_namespace" . }}
           path: /v1/webhook/conversion
-          port: 9443
+          port: 9501
       conversionReviewVersions:
       - v1beta2
       - v1beta1
@@ -1865,16 +1923,19 @@
                       items:
                         type: string
                       type: array
+                    diskType:
+                      enum:
+                      - filesystem
+                      - block
+                      type: string
                   type: object
                 type: object
-              engineManagerCPURequest:
-                type: integer
               evictionRequested:
                 type: boolean
+              instanceManagerCPURequest:
+                type: integer
               name:
                 type: string
-              replicaManagerCPURequest:
-                type: integer
               tags:
                 items:
                   type: string
@@ -1934,6 +1995,8 @@
                         type: object
                       nullable: true
                       type: array
+                    diskType:
+                      type: string
                     diskUUID:
                       type: string
                     scheduledReplica:
@@ -2153,7 +2216,7 @@
       jsonPath: .spec.groups
       name: Groups
       type: string
-    - description: Should be one of "snapshot", "snapshot-cleanup", "snapshot-delete" or "backup"
+    - description: Should be one of "snapshot", "snapshot-force-create", "snapshot-cleanup", "snapshot-delete", "backup", "backup-force-create" or "filesystem-trim"
       jsonPath: .spec.task
       name: Task
       type: string
@@ -2215,12 +2278,15 @@
                 description: The retain count of the snapshot/backup.
                 type: integer
               task:
-                description: The recurring job task. Can be "snapshot", "snapshot-cleanup", "snapshot-delete" or "backup".
+                description: The recurring job task. Can be "snapshot", "snapshot-force-create", "snapshot-cleanup", "snapshot-delete", "backup", "backup-force-create" or "filesystem-trim"
                 enum:
                 - snapshot
+                - snapshot-force-create
                 - snapshot-cleanup
                 - snapshot-delete
                 - backup
+                - backup-force-create
+                - filesystem-trim
                 type: string
             type: object
           status:
@@ -2350,16 +2416,15 @@
             properties:
               active:
                 type: boolean
+              backendStoreDriver:
+                enum:
+                - v1
+                - v2
+                type: string
               backingImage:
                 type: string
-              baseImage:
-                description: Deprecated. Rename to BackingImage
-                type: string
               dataDirectoryName:
                 type: string
-              dataPath:
-                description: Deprecated
-                type: string
               desireState:
                 type: string
               diskID:
@@ -2626,16 +2691,20 @@
             description: ShareManagerSpec defines the desired state of the Longhorn share manager
             properties:
               image:
+                description: Share manager image used for creating a share manager pod
                 type: string
             type: object
           status:
             description: ShareManagerStatus defines the observed state of the Longhorn share manager
             properties:
               endpoint:
+                description: NFS endpoint that can access the mounted filesystem of the volume
                 type: string
               ownerID:
+                description: The node ID on which the controller is responsible to reconcile this share manager resource
                 type: string
               state:
+                description: The state of the share manager resource
                 type: string
             type: object
         type: object
@@ -2947,6 +3016,11 @@
             type: object
           spec:
             description: SystemBackupSpec defines the desired state of the Longhorn SystemBackup
+            properties:
+              volumeBackupPolicy:
+                description: The create volume backup policy Can be "if-not-present", "always" or "disabled"
+                nullable: true
+                type: string
             type: object
           status:
             description: SystemBackupStatus defines the observed state of the Longhorn SystemBackup
@@ -3131,7 +3205,7 @@
           name: longhorn-conversion-webhook
           namespace: {{ include "release_namespace" . }}
           path: /v1/webhook/conversion
-          port: 9443
+          port: 9501
       conversionReviewVersions:
       - v1beta2
       - v1beta1
@@ -3238,10 +3312,18 @@
                 - rwo
                 - rwx
                 type: string
+              backendStoreDriver:
+                enum:
+                - v1
+                - v2
+                type: string
               backingImage:
                 type: string
-              baseImage:
-                description: Deprecated. Rename to BackingImage
+              backupCompressionMethod:
+                enum:
+                - none
+                - lz4
+                - gzip
                 type: string
               dataLocality:
                 enum:
@@ -3263,16 +3345,11 @@
                 type: string
               fromBackup:
                 type: string
-              restoreVolumeRecurringJob:
-                enum:
-                - ignored
-                - enabled
-                - disabled
-                type: string
               frontend:
                 enum:
                 - blockdev
                 - iscsi
+                - nvmf
                 - ""
                 type: string
               lastAttachedBy:
@@ -3289,36 +3366,13 @@
                 type: array
               numberOfReplicas:
                 type: integer
-              recurringJobs:
-                description: Deprecated. Replaced by a separate resource named "RecurringJob"
-                items:
-                  description: 'Deprecated: This field is useless and has been replaced by the RecurringJob CRD'
-                  properties:
-                    concurrency:
-                      type: integer
-                    cron:
-                      type: string
-                    groups:
-                      items:
-                        type: string
-                      type: array
-                    labels:
-                      additionalProperties:
-                        type: string
-                      type: object
-                    name:
-                      type: string
-                    retain:
-                      type: integer
-                    task:
-                      enum:
-                      - snapshot
-                      - snapshot-cleanup
-                      - snapshot-delete
-                      - backup
-                      type: string
-                  type: object
-                type: array
+              offlineReplicaRebuilding:
+                description: OfflineReplicaRebuilding is used to determine if the offline replica rebuilding feature is enabled or not
+                enum:
+                - ignored
+                - disabled
+                - enabled
+                type: string
               replicaAutoBalance:
                 enum:
                 - ignored
@@ -3326,6 +3380,26 @@
                 - least-effort
                 - best-effort
                 type: string
+              replicaSoftAntiAffinity:
+                description: Replica soft anti affinity of the volume. Set enabled to allow replicas to be scheduled on the same node
+                enum:
+                - ignored
+                - enabled
+                - disabled
+                type: string
+              replicaZoneSoftAntiAffinity:
+                description: Replica zone soft anti affinity of the volume. Set enabled to allow replicas to be scheduled in the same zone
+                enum:
+                - ignored
+                - enabled
+                - disabled
+                type: string
+              restoreVolumeRecurringJob:
+                enum:
+                - ignored
+                - enabled
+                - disabled
+                type: string
               revisionCounterDisabled:
                 type: boolean
               size:
@@ -3388,6 +3462,9 @@
                 type: array
               currentImage:
                 type: string
+              currentMigrationNodeID:
+                description: the node that this volume is currently migrating to
+                type: string
               currentNodeID:
                 type: string
               expansionRequired:
@@ -3433,9 +3510,12 @@
                 type: string
               lastDegradedAt:
                 type: string
+              offlineReplicaRebuildingRequired:
+                type: boolean
               ownerID:
                 type: string
               pendingNodeID:
+                description: Deprecated.
                 type: string
               remountRequestedAt:
                 type: string
@@ -3463,3 +3543,130 @@
     plural: ""
   conditions: []
   storedVersions: []
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+    longhorn-manager: ""
+  name: volumeattachments.longhorn.io
+spec:
+  group: longhorn.io
+  names:
+    kind: VolumeAttachment
+    listKind: VolumeAttachmentList
+    plural: volumeattachments
+    shortNames:
+      - lhva
+    singular: volumeattachment
+  scope: Namespaced
+  versions:
+    - additionalPrinterColumns:
+        - jsonPath: .metadata.creationTimestamp
+          name: Age
+          type: date
+      name: v1beta2
+      schema:
+        openAPIV3Schema:
+          description: VolumeAttachment stores attachment information of a Longhorn volume
+          properties:
+            apiVersion:
+              description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+              type: string
+            kind:
+              description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+              type: string
+            metadata:
+              type: object
+            spec:
+              description: VolumeAttachmentSpec defines the desired state of Longhorn VolumeAttachment
+              properties:
+                attachmentTickets:
+                  additionalProperties:
+                    properties:
+                      generation:
+                        description: A sequence number representing a specific generation of the desired state. Populated by the system. Read-only.
+                        format: int64
+                        type: integer
+                      id:
+                        description: The unique ID of this attachment. Used to differentiate different attachments of the same volume.
+                        type: string
+                      nodeID:
+                        description: The node that this attachment is requesting
+                        type: string
+                      parameters:
+                        additionalProperties:
+                          type: string
+                        description: Optional additional parameter for this attachment
+                        type: object
+                      type:
+                        type: string
+                    type: object
+                  type: object
+                volume:
+                  description: The name of Longhorn volume of this VolumeAttachment
+                  type: string
+              required:
+                - volume
+              type: object
+            status:
+              description: VolumeAttachmentStatus defines the observed state of Longhorn VolumeAttachment
+              properties:
+                attachmentTicketStatuses:
+                  additionalProperties:
+                    properties:
+                      conditions:
+                        description: Record any error when trying to fulfill this attachment
+                        items:
+                          properties:
+                            lastProbeTime:
+                              description: Last time we probed the condition.
+                              type: string
+                            lastTransitionTime:
+                              description: Last time the condition transitioned from one status to another.
+                              type: string
+                            message:
+                              description: Human-readable message indicating details about last transition.
+                              type: string
+                            reason:
+                              description: Unique, one-word, CamelCase reason for the condition's last transition.
+                              type: string
+                            status:
+                              description: Status is the status of the condition. Can be True, False, Unknown.
+                              type: string
+                            type:
+                              description: Type is the type of the condition.
+                              type: string
+                          type: object
+                        nullable: true
+                        type: array
+                      generation:
+                        description: A sequence number representing a specific generation of the desired state. Populated by the system. Read-only.
+                        format: int64
+                        type: integer
+                      id:
+                        description: The unique ID of this attachment. Used to differentiate different attachments of the same volume.
+                        type: string
+                      satisfied:
+                        description: Indicate whether this attachment ticket has been satisfied
+                        type: boolean
+                    required:
+                      - conditions
+                      - satisfied
+                    type: object
+                  type: object
+              type: object
+          type: object
+      served: true
+      storage: true
+      subresources:
+        status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
diff --git a/charts/longhorn/templates/daemonset-sa.yaml b/charts/longhorn/templates/daemonset-sa.yaml
index 63f98cd..f361d27 100644
--- a/charts/longhorn/templates/daemonset-sa.yaml
+++ b/charts/longhorn/templates/daemonset-sa.yaml
@@ -18,10 +18,6 @@
         {{- toYaml . | nindent 8 }}
       {{- end }}
     spec:
-      initContainers:
-      - name: wait-longhorn-admission-webhook
-        image: {{ template "registry_url" . }}{{ .Values.image.longhorn.manager.repository }}:{{ .Values.image.longhorn.manager.tag }}
-        command: ['sh', '-c', 'while [ $(curl -m 1 -s -o /dev/null -w "%{http_code}" -k https://longhorn-admission-webhook:9443/v1/healthz) != "200" ]; do echo waiting; sleep 2; done']
       containers:
       - name: longhorn-manager
         image: {{ template "registry_url" . }}{{ .Values.image.longhorn.manager.repository }}:{{ .Values.image.longhorn.manager.tag }}
@@ -52,9 +48,17 @@
         ports:
         - containerPort: 9500
           name: manager
+        - containerPort: 9501
+          name: conversion-wh
+        - containerPort: 9502
+          name: admission-wh
+        - containerPort: 9503
+          name: recov-backend
         readinessProbe:
-          tcpSocket:
-            port: 9500
+          httpGet:
+            path: /v1/healthz
+            port: 9501
+            scheme: HTTPS
         volumeMounts:
         - name: dev
           mountPath: /host/dev/
diff --git a/charts/longhorn/templates/default-setting.yaml b/charts/longhorn/templates/default-setting.yaml
index 49870a4..ac38ba9 100644
--- a/charts/longhorn/templates/default-setting.yaml
+++ b/charts/longhorn/templates/default-setting.yaml
@@ -15,6 +15,7 @@
     {{ if not (kindIs "invalid" .Values.defaultSettings.replicaAutoBalance) }}replica-auto-balance: {{ .Values.defaultSettings.replicaAutoBalance }}{{ end }}
     {{ if not (kindIs "invalid" .Values.defaultSettings.storageOverProvisioningPercentage) }}storage-over-provisioning-percentage: {{ .Values.defaultSettings.storageOverProvisioningPercentage }}{{ end }}
     {{ if not (kindIs "invalid" .Values.defaultSettings.storageMinimalAvailablePercentage) }}storage-minimal-available-percentage: {{ .Values.defaultSettings.storageMinimalAvailablePercentage }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.storageReservedPercentageForDefaultDisk) }}storage-reserved-percentage-for-default-disk: {{ .Values.defaultSettings.storageReservedPercentageForDefaultDisk }}{{ end }}
     {{ if not (kindIs "invalid" .Values.defaultSettings.upgradeChecker) }}upgrade-checker: {{ .Values.defaultSettings.upgradeChecker }}{{ end }}
     {{ if not (kindIs "invalid" .Values.defaultSettings.defaultReplicaCount) }}default-replica-count: {{ .Values.defaultSettings.defaultReplicaCount }}{{ end }}
     {{ if not (kindIs "invalid" .Values.defaultSettings.defaultDataLocality) }}default-data-locality: {{ .Values.defaultSettings.defaultDataLocality }}{{ end }}
@@ -51,9 +52,7 @@
     {{ if not (kindIs "invalid" .Values.defaultSettings.disableSchedulingOnCordonedNode) }}disable-scheduling-on-cordoned-node: {{ .Values.defaultSettings.disableSchedulingOnCordonedNode }}{{ end }}
     {{ if not (kindIs "invalid" .Values.defaultSettings.replicaZoneSoftAntiAffinity) }}replica-zone-soft-anti-affinity: {{ .Values.defaultSettings.replicaZoneSoftAntiAffinity }}{{ end }}
     {{ if not (kindIs "invalid" .Values.defaultSettings.nodeDownPodDeletionPolicy) }}node-down-pod-deletion-policy: {{ .Values.defaultSettings.nodeDownPodDeletionPolicy }}{{ end }}
-    {{ if not (kindIs "invalid" .Values.defaultSettings.allowNodeDrainWithLastHealthyReplica) }}allow-node-drain-with-last-healthy-replica: {{ .Values.defaultSettings.allowNodeDrainWithLastHealthyReplica }}{{ end }}
-    {{ if not (kindIs "invalid" .Values.defaultSettings.mkfsExt4Parameters) }}mkfs-ext4-parameters: {{ .Values.defaultSettings.mkfsExt4Parameters }}{{ end }}
-    {{ if not (kindIs "invalid" .Values.defaultSettings.disableReplicaRebuild) }}disable-replica-rebuild: {{ .Values.defaultSettings.disableReplicaRebuild }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.nodeDrainPolicy) }}node-drain-policy: {{ .Values.defaultSettings.nodeDrainPolicy }}{{ end }}
     {{ if not (kindIs "invalid" .Values.defaultSettings.replicaReplenishmentWaitInterval) }}replica-replenishment-wait-interval: {{ .Values.defaultSettings.replicaReplenishmentWaitInterval }}{{ end }}
     {{ if not (kindIs "invalid" .Values.defaultSettings.concurrentReplicaRebuildPerNodeLimit) }}concurrent-replica-rebuild-per-node-limit: {{ .Values.defaultSettings.concurrentReplicaRebuildPerNodeLimit }}{{ end }}
     {{ if not (kindIs "invalid" .Values.defaultSettings.concurrentVolumeBackupRestorePerNodeLimit) }}concurrent-volume-backup-restore-per-node-limit: {{ .Values.defaultSettings.concurrentVolumeBackupRestorePerNodeLimit }}{{ end }}
@@ -64,8 +63,7 @@
     {{ if not (kindIs "invalid" .Values.defaultSettings.concurrentAutomaticEngineUpgradePerNodeLimit) }}concurrent-automatic-engine-upgrade-per-node-limit: {{ .Values.defaultSettings.concurrentAutomaticEngineUpgradePerNodeLimit }}{{ end }}
     {{ if not (kindIs "invalid" .Values.defaultSettings.backingImageCleanupWaitInterval) }}backing-image-cleanup-wait-interval: {{ .Values.defaultSettings.backingImageCleanupWaitInterval }}{{ end }}
     {{ if not (kindIs "invalid" .Values.defaultSettings.backingImageRecoveryWaitInterval) }}backing-image-recovery-wait-interval: {{ .Values.defaultSettings.backingImageRecoveryWaitInterval }}{{ end }}
-    {{ if not (kindIs "invalid" .Values.defaultSettings.guaranteedEngineManagerCPU) }}guaranteed-engine-manager-cpu: {{ .Values.defaultSettings.guaranteedEngineManagerCPU }}{{ end }}
-    {{ if not (kindIs "invalid" .Values.defaultSettings.guaranteedReplicaManagerCPU) }}guaranteed-replica-manager-cpu: {{ .Values.defaultSettings.guaranteedReplicaManagerCPU }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.guaranteedInstanceManagerCPU) }}guaranteed-instance-manager-cpu: {{ .Values.defaultSettings.guaranteedInstanceManagerCPU }}{{ end }}
     {{ if not (kindIs "invalid" .Values.defaultSettings.kubernetesClusterAutoscalerEnabled) }}kubernetes-cluster-autoscaler-enabled: {{ .Values.defaultSettings.kubernetesClusterAutoscalerEnabled }}{{ end }}
     {{ if not (kindIs "invalid" .Values.defaultSettings.orphanAutoDeletion) }}orphan-auto-deletion: {{ .Values.defaultSettings.orphanAutoDeletion }}{{ end }}
     {{ if not (kindIs "invalid" .Values.defaultSettings.storageNetwork) }}storage-network: {{ .Values.defaultSettings.storageNetwork }}{{ end }}
@@ -76,4 +74,10 @@
     {{ if not (kindIs "invalid" .Values.defaultSettings.snapshotDataIntegrityCronjob) }}snapshot-data-integrity-cronjob: {{ .Values.defaultSettings.snapshotDataIntegrityCronjob }}{{ end }}
     {{ if not (kindIs "invalid" .Values.defaultSettings.removeSnapshotsDuringFilesystemTrim) }}remove-snapshots-during-filesystem-trim: {{ .Values.defaultSettings.removeSnapshotsDuringFilesystemTrim }}{{ end }}
     {{ if not (kindIs "invalid" .Values.defaultSettings.fastReplicaRebuildEnabled) }}fast-replica-rebuild-enabled: {{ .Values.defaultSettings.fastReplicaRebuildEnabled }}{{ end }}
-    {{ if not (kindIs "invalid" .Values.defaultSettings.replicaFileSyncHttpClientTimeout) }}replica-file-sync-http-client-timeout: {{ .Values.defaultSettings.replicaFileSyncHttpClientTimeout }}{{ end }}
\ No newline at end of file
+    {{ if not (kindIs "invalid" .Values.defaultSettings.replicaFileSyncHttpClientTimeout) }}replica-file-sync-http-client-timeout: {{ .Values.defaultSettings.replicaFileSyncHttpClientTimeout }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.logLevel) }}log-level: {{ .Values.defaultSettings.logLevel }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.backupCompressionMethod) }}backup-compression-method: {{ .Values.defaultSettings.backupCompressionMethod }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.backupConcurrentLimit) }}backup-concurrent-limit: {{ .Values.defaultSettings.backupConcurrentLimit }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.restoreConcurrentLimit) }}restore-concurrent-limit: {{ .Values.defaultSettings.restoreConcurrentLimit }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.v2DataEngine) }}v2-data-engine: {{ .Values.defaultSettings.v2DataEngine }}{{ end }}
+    {{ if not (kindIs "invalid" .Values.defaultSettings.offlineReplicaRebuilding) }}offline-replica-rebuilding: {{ .Values.defaultSettings.offlineReplicaRebuilding }}{{ end }}
diff --git a/charts/longhorn/templates/network-policies/backing-image-data-source-network-policy.yaml b/charts/longhorn/templates/network-policies/backing-image-data-source-network-policy.yaml
new file mode 100644
index 0000000..cc91054
--- /dev/null
+++ b/charts/longhorn/templates/network-policies/backing-image-data-source-network-policy.yaml
@@ -0,0 +1,27 @@
+{{- if .Values.networkPolicies.enabled }}
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: backing-image-data-source
+  namespace: longhorn-system
+spec:
+  podSelector:
+    matchLabels:
+      longhorn.io/component: backing-image-data-source
+  policyTypes:
+  - Ingress
+  ingress:
+  - from:
+    - podSelector:
+        matchLabels:
+          app: longhorn-manager
+    - podSelector:
+        matchLabels:
+          longhorn.io/component: instance-manager
+    - podSelector:
+        matchLabels:
+          longhorn.io/component: backing-image-manager
+    - podSelector:
+        matchLabels:
+          longhorn.io/component: backing-image-data-source
+{{- end }}
diff --git a/charts/longhorn/templates/network-policies/backing-image-manager-network-policy.yaml b/charts/longhorn/templates/network-policies/backing-image-manager-network-policy.yaml
new file mode 100644
index 0000000..ebc288f
--- /dev/null
+++ b/charts/longhorn/templates/network-policies/backing-image-manager-network-policy.yaml
@@ -0,0 +1,27 @@
+{{- if .Values.networkPolicies.enabled }}
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: backing-image-manager
+  namespace: longhorn-system
+spec:
+  podSelector:
+    matchLabels:
+      longhorn.io/component: backing-image-manager
+  policyTypes:
+  - Ingress
+  ingress:
+  - from:
+    - podSelector:
+        matchLabels:
+          app: longhorn-manager
+    - podSelector:
+        matchLabels:
+          longhorn.io/component: instance-manager
+    - podSelector:
+        matchLabels:
+          longhorn.io/component: backing-image-manager
+    - podSelector:
+        matchLabels:
+          longhorn.io/component: backing-image-data-source
+{{- end }}
diff --git a/charts/longhorn/templates/network-policies/instance-manager-networking.yaml b/charts/longhorn/templates/network-policies/instance-manager-networking.yaml
new file mode 100644
index 0000000..6f03c6e
--- /dev/null
+++ b/charts/longhorn/templates/network-policies/instance-manager-networking.yaml
@@ -0,0 +1,27 @@
+{{- if .Values.networkPolicies.enabled }}
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: instance-manager
+  namespace: longhorn-system
+spec:
+  podSelector:
+    matchLabels:
+      longhorn.io/component: instance-manager
+  policyTypes:
+  - Ingress
+  ingress:
+  - from:
+    - podSelector:
+        matchLabels:
+          app: longhorn-manager
+    - podSelector:
+        matchLabels:
+          longhorn.io/component: instance-manager
+    - podSelector:
+        matchLabels:
+          longhorn.io/component: backing-image-manager
+    - podSelector:
+        matchLabels:
+          longhorn.io/component: backing-image-data-source
+{{- end }}
diff --git a/charts/longhorn/templates/network-policies/manager-network-policy.yaml b/charts/longhorn/templates/network-policies/manager-network-policy.yaml
new file mode 100644
index 0000000..c9d763f
--- /dev/null
+++ b/charts/longhorn/templates/network-policies/manager-network-policy.yaml
@@ -0,0 +1,35 @@
+{{- if .Values.networkPolicies.enabled }}
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: longhorn-manager
+  namespace: longhorn-system
+spec:
+  podSelector:
+    matchLabels:
+      app: longhorn-manager
+  policyTypes:
+  - Ingress
+  ingress:
+  - from:
+    - podSelector:
+        matchLabels:
+          app: longhorn-manager
+    - podSelector:
+        matchLabels:
+          app: longhorn-ui
+    - podSelector:
+        matchLabels:
+          app: longhorn-csi-plugin
+    - podSelector:
+        matchLabels:
+          longhorn.io/managed-by: longhorn-manager
+        matchExpressions:
+          - { key: recurring-job.longhorn.io, operator: Exists }
+    - podSelector:
+        matchExpressions:
+          - { key: longhorn.io/job-task, operator: Exists }
+    - podSelector:
+        matchLabels:
+          app: longhorn-driver-deployer
+{{- end }}
diff --git a/charts/longhorn/templates/network-policies/recovery-backend-network-policy.yaml b/charts/longhorn/templates/network-policies/recovery-backend-network-policy.yaml
new file mode 100644
index 0000000..cebe485
--- /dev/null
+++ b/charts/longhorn/templates/network-policies/recovery-backend-network-policy.yaml
@@ -0,0 +1,17 @@
+{{- if .Values.networkPolicies.enabled }}
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: longhorn-recovery-backend
+  namespace: longhorn-system
+spec:
+  podSelector:
+    matchLabels:
+      app: longhorn-manager
+  policyTypes:
+  - Ingress
+  ingress:
+  - ports:
+    - protocol: TCP
+      port: 9503
+{{- end }}
diff --git a/charts/longhorn/templates/network-policies/ui-frontend-network-policy.yaml b/charts/longhorn/templates/network-policies/ui-frontend-network-policy.yaml
new file mode 100644
index 0000000..04c8beb
--- /dev/null
+++ b/charts/longhorn/templates/network-policies/ui-frontend-network-policy.yaml
@@ -0,0 +1,46 @@
+{{- if and .Values.networkPolicies.enabled .Values.ingress.enabled (not (eq .Values.networkPolicies.type "")) }}
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: longhorn-ui-frontend
+  namespace: longhorn-system
+spec:
+  podSelector:
+    matchLabels:
+      app: longhorn-ui
+  policyTypes:
+  - Ingress
+  ingress:
+  - from:
+    {{- if eq .Values.networkPolicies.type "rke1"}}
+    - namespaceSelector:
+        matchLabels:
+          kubernetes.io/metadata.name: ingress-nginx
+      podSelector:
+        matchLabels:
+          app.kubernetes.io/component: controller
+          app.kubernetes.io/instance: ingress-nginx
+          app.kubernetes.io/name: ingress-nginx
+    {{- else if eq .Values.networkPolicies.type "rke2" }}
+    - namespaceSelector:
+        matchLabels:
+          kubernetes.io/metadata.name: kube-system
+      podSelector:
+        matchLabels:
+          app.kubernetes.io/component: controller
+          app.kubernetes.io/instance: rke2-ingress-nginx
+          app.kubernetes.io/name: rke2-ingress-nginx
+    {{- else if eq .Values.networkPolicies.type "k3s" }}
+    - namespaceSelector:
+        matchLabels:
+          kubernetes.io/metadata.name: kube-system
+      podSelector:
+        matchLabels:
+          app.kubernetes.io/name: traefik
+    ports:
+      - port: 8000
+        protocol: TCP
+      - port: 80
+        protocol: TCP
+    {{- end }}
+{{- end }}
diff --git a/charts/longhorn/templates/network-policies/webhook-network-policy.yaml b/charts/longhorn/templates/network-policies/webhook-network-policy.yaml
new file mode 100644
index 0000000..c9790f6
--- /dev/null
+++ b/charts/longhorn/templates/network-policies/webhook-network-policy.yaml
@@ -0,0 +1,33 @@
+{{- if .Values.networkPolicies.enabled }}
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: longhorn-conversion-webhook
+  namespace: longhorn-system
+spec:
+  podSelector:
+    matchLabels:
+      app: longhorn-manager
+  policyTypes:
+  - Ingress
+  ingress:
+  - ports:
+    - protocol: TCP
+      port: 9501
+---
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: longhorn-admission-webhook
+  namespace: longhorn-system
+spec:
+  podSelector:
+    matchLabels:
+      app: longhorn-manager
+  policyTypes:
+  - Ingress
+  ingress:
+  - ports:
+    - protocol: TCP
+      port: 9502
+{{- end }}
diff --git a/charts/longhorn/templates/postupgrade-job.yaml b/charts/longhorn/templates/postupgrade-job.yaml
index b9b2eeb..bb25a54 100644
--- a/charts/longhorn/templates/postupgrade-job.yaml
+++ b/charts/longhorn/templates/postupgrade-job.yaml
@@ -19,8 +19,6 @@
       - name: longhorn-post-upgrade
         image: {{ template "registry_url" . }}{{ .Values.image.longhorn.manager.repository }}:{{ .Values.image.longhorn.manager.tag }}
         imagePullPolicy: {{ .Values.image.pullPolicy }}
-        securityContext:
-          privileged: true
         command:
         - longhorn-manager
         - post-upgrade
diff --git a/charts/longhorn/templates/preupgrade-job.yaml b/charts/longhorn/templates/preupgrade-job.yaml
new file mode 100644
index 0000000..357e6d7
--- /dev/null
+++ b/charts/longhorn/templates/preupgrade-job.yaml
@@ -0,0 +1,58 @@
+{{- if .Values.helmPreUpgradeCheckerJob.enabled }}
+apiVersion: batch/v1
+kind: Job
+metadata:
+  annotations:
+    "helm.sh/hook": pre-upgrade
+    "helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation,hook-failed
+  name: longhorn-pre-upgrade
+  namespace: {{ include "release_namespace" . }}
+  labels: {{- include "longhorn.labels" . | nindent 4 }}
+spec:
+  activeDeadlineSeconds: 900
+  backoffLimit: 1
+  template:
+    metadata:
+      name: longhorn-pre-upgrade
+      labels: {{- include "longhorn.labels" . | nindent 8 }}
+    spec:
+      containers:
+      - name: longhorn-pre-upgrade
+        image: {{ template "registry_url" . }}{{ .Values.image.longhorn.manager.repository }}:{{ .Values.image.longhorn.manager.tag }}
+        imagePullPolicy: {{ .Values.image.pullPolicy }}
+        command:
+        - longhorn-manager
+        - pre-upgrade
+        env:
+        - name: POD_NAMESPACE
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.namespace
+      restartPolicy: OnFailure
+      {{- if .Values.privateRegistry.registrySecret }}
+      imagePullSecrets:
+      - name: {{ .Values.privateRegistry.registrySecret }}
+      {{- end }}
+      {{- if .Values.longhornManager.priorityClass }}
+      priorityClassName: {{ .Values.longhornManager.priorityClass | quote }}
+      {{- end }}
+      serviceAccountName: longhorn-service-account
+      {{- if or .Values.longhornManager.tolerations .Values.global.cattle.windowsCluster.enabled }}
+      tolerations:
+        {{- if and .Values.global.cattle.windowsCluster.enabled .Values.global.cattle.windowsCluster.tolerations }}
+{{ toYaml .Values.global.cattle.windowsCluster.tolerations | indent 6 }}
+        {{- end }}
+        {{- if .Values.longhornManager.tolerations }}
+{{ toYaml .Values.longhornManager.tolerations | indent 6 }}
+        {{- end }}
+      {{- end }}
+      {{- if or .Values.longhornManager.nodeSelector .Values.global.cattle.windowsCluster.enabled }}
+      nodeSelector:
+        {{- if and .Values.global.cattle.windowsCluster.enabled .Values.global.cattle.windowsCluster.nodeSelector }}
+{{ toYaml .Values.global.cattle.windowsCluster.nodeSelector | indent 8 }}
+        {{- end }}
+        {{- if .Values.longhornManager.nodeSelector }}
+{{ toYaml .Values.longhornManager.nodeSelector | indent 8 }}
+        {{- end }}
+      {{- end }}
+{{- end }}
\ No newline at end of file
diff --git a/charts/longhorn/templates/services.yaml b/charts/longhorn/templates/services.yaml
index cd008db..7da9d18 100644
--- a/charts/longhorn/templates/services.yaml
+++ b/charts/longhorn/templates/services.yaml
@@ -9,10 +9,10 @@
   type: ClusterIP
   sessionAffinity: ClientIP
   selector:
-    app: longhorn-conversion-webhook
+    app: longhorn-manager
   ports:
   - name: conversion-webhook
-    port: 9443
+    port: 9501
     targetPort: conversion-wh
 ---
 apiVersion: v1
@@ -26,10 +26,10 @@
   type: ClusterIP
   sessionAffinity: ClientIP
   selector:
-    app: longhorn-admission-webhook
+    app: longhorn-manager
   ports:
   - name: admission-webhook
-    port: 9443
+    port: 9502
     targetPort: admission-wh
 ---
 apiVersion: v1
@@ -43,10 +43,10 @@
   type: ClusterIP
   sessionAffinity: ClientIP
   selector:
-    app: longhorn-recovery-backend
+    app: longhorn-manager
   ports:
   - name: recovery-backend
-    port: 9600
+    port: 9503
     targetPort: recov-backend
 ---
 apiVersion: v1
diff --git a/charts/longhorn/templates/uninstall-job.yaml b/charts/longhorn/templates/uninstall-job.yaml
index 989933d..968f420 100644
--- a/charts/longhorn/templates/uninstall-job.yaml
+++ b/charts/longhorn/templates/uninstall-job.yaml
@@ -19,8 +19,6 @@
       - name: longhorn-uninstall
         image: {{ template "registry_url" . }}{{ .Values.image.longhorn.manager.repository }}:{{ .Values.image.longhorn.manager.tag }}
         imagePullPolicy: {{ .Values.image.pullPolicy }}
-        securityContext:
-          privileged: true
         command:
         - longhorn-manager
         - uninstall
diff --git a/charts/longhorn/values.yaml b/charts/longhorn/values.yaml
index 3ded6cd..bad9882 100644
--- a/charts/longhorn/values.yaml
+++ b/charts/longhorn/values.yaml
@@ -21,48 +21,53 @@
         taintToleration: cattle.io/os=linux:NoSchedule
         systemManagedComponentsNodeSelector: kubernetes.io/os:linux
 
+networkPolicies:
+  enabled: false
+  # Available types: k3s, rke2, rke1
+  type: "k3s"
+
 image:
   longhorn:
     engine:
       repository: longhornio/longhorn-engine
-      tag: v1.4.1
+      tag: v1.5.2
     manager:
       repository: longhornio/longhorn-manager
-      tag: v1.4.1
+      tag: v1.5.2
     ui:
       repository: longhornio/longhorn-ui
-      tag: v1.4.1
+      tag: v1.5.2
     instanceManager:
       repository: longhornio/longhorn-instance-manager
-      tag: v1.4.1
+      tag: v1.5.2
     shareManager:
       repository: longhornio/longhorn-share-manager
-      tag: v1.4.1
+      tag: v1.5.2
     backingImageManager:
       repository: longhornio/backing-image-manager
-      tag: v1.4.1
+      tag: v1.5.2
     supportBundleKit:
       repository: longhornio/support-bundle-kit
-      tag: v0.0.19
+      tag: v0.0.27
   csi:
     attacher:
       repository: longhornio/csi-attacher
-      tag: v3.4.0
+      tag: v4.2.0
     provisioner:
       repository: longhornio/csi-provisioner
-      tag: v2.1.2
+      tag: v3.4.1
     nodeDriverRegistrar:
       repository: longhornio/csi-node-driver-registrar
-      tag: v2.5.0
+      tag: v2.7.0
     resizer:
       repository: longhornio/csi-resizer
-      tag: v1.3.0
+      tag: v1.7.0
     snapshotter:
       repository: longhornio/csi-snapshotter
-      tag: v5.0.1
+      tag: v6.2.1
     livenessProbe:
       repository: longhornio/livenessprobe
-      tag: v2.8.0
+      tag: v2.9.0
   pullPolicy: IfNotPresent
 
 service:
@@ -94,9 +99,12 @@
     expectedChecksum: ~
   defaultNodeSelector:
     enable: false # disable by default
-    selector: []
+    selector: ""
   removeSnapshotsDuringFilesystemTrim: ignored # "enabled" or "disabled" otherwise
 
+helmPreUpgradeCheckerJob:
+  enabled: true
+
 csi:
   kubeletRootDir: ~
   attacherReplicaCount: ~
@@ -115,6 +123,7 @@
   replicaAutoBalance: ~
   storageOverProvisioningPercentage: ~
   storageMinimalAvailablePercentage: ~
+  storageReservedPercentageForDefaultDisk: ~
   upgradeChecker: ~
   defaultReplicaCount: ~
   defaultLonghornStaticStorageClass: ~
@@ -132,9 +141,7 @@
   disableSchedulingOnCordonedNode: ~
   replicaZoneSoftAntiAffinity: ~
   nodeDownPodDeletionPolicy: ~
-  allowNodeDrainWithLastHealthyReplica: ~
-  mkfsExt4Parameters: ~
-  disableReplicaRebuild: ~
+  nodeDrainPolicy: ~
   replicaReplenishmentWaitInterval: ~
   concurrentReplicaRebuildPerNodeLimit: ~
   concurrentVolumeBackupRestorePerNodeLimit: ~
@@ -145,8 +152,7 @@
   concurrentAutomaticEngineUpgradePerNodeLimit: ~
   backingImageCleanupWaitInterval: ~
   backingImageRecoveryWaitInterval: ~
-  guaranteedEngineManagerCPU: ~
-  guaranteedReplicaManagerCPU: ~
+  guaranteedInstanceManagerCPU: ~
   kubernetesClusterAutoscalerEnabled: ~
   orphanAutoDeletion: ~
   storageNetwork: ~
@@ -158,6 +164,12 @@
   removeSnapshotsDuringFilesystemTrim: ~
   fastReplicaRebuildEnabled: ~
   replicaFileSyncHttpClientTimeout: ~
+  logLevel: ~
+  backupCompressionMethod: ~
+  backupConcurrentLimit: ~
+  restoreConcurrentLimit: ~
+  v2DataEngine: ~
+  offlineReplicaRebuilding: ~
 privateRegistry:
   createSecret: ~
   registryUrl: ~
@@ -219,54 +231,6 @@
   #  label-key1: "label-value1"
   #  label-key2: "label-value2"
 
-longhornConversionWebhook:
-  replicas: 2
-  priorityClass: ~
-  tolerations: []
-  ## If you want to set tolerations for Longhorn conversion webhook Deployment, delete the `[]` in the line above
-  ## and uncomment this example block
-  # - key: "key"
-  #   operator: "Equal"
-  #   value: "value"
-  #   effect: "NoSchedule"
-  nodeSelector: {}
-  ## If you want to set node selector for Longhorn conversion webhook Deployment, delete the `{}` in the line above
-  ## and uncomment this example block
-  #  label-key1: "label-value1"
-  #  label-key2: "label-value2"
-
-longhornAdmissionWebhook:
-  replicas: 2
-  priorityClass: ~
-  tolerations: []
-  ## If you want to set tolerations for Longhorn admission webhook Deployment, delete the `[]` in the line above
-  ## and uncomment this example block
-  # - key: "key"
-  #   operator: "Equal"
-  #   value: "value"
-  #   effect: "NoSchedule"
-  nodeSelector: {}
-  ## If you want to set node selector for Longhorn admission webhook Deployment, delete the `{}` in the line above
-  ## and uncomment this example block
-  #  label-key1: "label-value1"
-  #  label-key2: "label-value2"
-
-longhornRecoveryBackend:
-  replicas: 2
-  priorityClass: ~
-  tolerations: []
-  ## If you want to set tolerations for Longhorn recovery backend Deployment, delete the `[]` in the line above
-  ## and uncomment this example block
-  # - key: "key"
-  #   operator: "Equal"
-  #   value: "value"
-  #   effect: "NoSchedule"
-  nodeSelector: {}
-  ## If you want to set node selector for Longhorn recovery backend Deployment, delete the `{}` in the line above
-  ## and uncomment this example block
-  #  label-key1: "label-value1"
-  #  label-key2: "label-value2"
-
 ingress:
   ## Set to true to enable ingress record generation
   enabled: false
diff --git a/charts/metallb-0.13.7/.helmignore b/charts/metallb-0.13.7/.helmignore
new file mode 100644
index 0000000..0e8a0eb
--- /dev/null
+++ b/charts/metallb-0.13.7/.helmignore
@@ -0,0 +1,23 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*.orig
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
+.vscode/
diff --git a/charts/metallb-0.13.7/Chart.yaml b/charts/metallb-0.13.7/Chart.yaml
new file mode 100644
index 0000000..76e774d
--- /dev/null
+++ b/charts/metallb-0.13.7/Chart.yaml
@@ -0,0 +1,11 @@
+apiVersion: v2
+appVersion: v0.13.7
+description: A network load-balancer implementation for Kubernetes using standard
+  routing protocols
+home: https://metallb.universe.tf
+icon: https://metallb.universe.tf/images/logo/metallb-white.png
+name: metallb
+sources:
+- https://github.com/metallb/metallb
+type: application
+version: 0.13.7
diff --git a/charts/metallb-0.13.7/README.md b/charts/metallb-0.13.7/README.md
new file mode 100644
index 0000000..25cb5d4
--- /dev/null
+++ b/charts/metallb-0.13.7/README.md
@@ -0,0 +1,148 @@
+# metallb
+
+![Version: 0.0.0](https://img.shields.io/badge/Version-0.0.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v0.0.0](https://img.shields.io/badge/AppVersion-v0.0.0-informational?style=flat-square)
+
+A network load-balancer implementation for Kubernetes using standard routing protocols
+
+**Homepage:** <https://metallb.universe.tf>
+
+## Source Code
+
+* <https://github.com/metallb/metallb>
+
+## Requirements
+
+| Repository | Name | Version |
+|------------|------|---------|
+|  | crds | 0.0.0 |
+
+## Values
+
+| Key | Type | Default | Description |
+|-----|------|---------|-------------|
+| controller.affinity | object | `{}` |  |
+| controller.enabled | bool | `true` |  |
+| controller.image.pullPolicy | string | `nil` |  |
+| controller.image.repository | string | `"quay.io/metallb/controller"` |  |
+| controller.image.tag | string | `nil` |  |
+| controller.livenessProbe.enabled | bool | `true` |  |
+| controller.livenessProbe.failureThreshold | int | `3` |  |
+| controller.livenessProbe.initialDelaySeconds | int | `10` |  |
+| controller.livenessProbe.periodSeconds | int | `10` |  |
+| controller.livenessProbe.successThreshold | int | `1` |  |
+| controller.livenessProbe.timeoutSeconds | int | `1` |  |
+| controller.logLevel | string | `"info"` | Controller log level. Must be one of: `all`, `debug`, `info`, `warn`, `error` or `none` |
+| controller.nodeSelector | object | `{}` |  |
+| controller.podAnnotations | object | `{}` |  |
+| controller.priorityClassName | string | `""` |  |
+| controller.readinessProbe.enabled | bool | `true` |  |
+| controller.readinessProbe.failureThreshold | int | `3` |  |
+| controller.readinessProbe.initialDelaySeconds | int | `10` |  |
+| controller.readinessProbe.periodSeconds | int | `10` |  |
+| controller.readinessProbe.successThreshold | int | `1` |  |
+| controller.readinessProbe.timeoutSeconds | int | `1` |  |
+| controller.resources | object | `{}` |  |
+| controller.runtimeClassName | string | `""` |  |
+| controller.securityContext.fsGroup | int | `65534` |  |
+| controller.securityContext.runAsNonRoot | bool | `true` |  |
+| controller.securityContext.runAsUser | int | `65534` |  |
+| controller.serviceAccount.annotations | object | `{}` |  |
+| controller.serviceAccount.create | bool | `true` |  |
+| controller.serviceAccount.name | string | `""` |  |
+| controller.strategy.type | string | `"RollingUpdate"` |  |
+| controller.tolerations | list | `[]` |  |
+| crds.enabled | bool | `true` |  |
+| crds.validationFailurePolicy | string | `"Fail"` |  |
+| fullnameOverride | string | `""` |  |
+| imagePullSecrets | list | `[]` |  |
+| loadBalancerClass | string | `""` |  |
+| nameOverride | string | `""` |  |
+| prometheus.controllerMetricsTLSSecret | string | `""` |  |
+| prometheus.metricsPort | int | `7472` |  |
+| prometheus.namespace | string | `""` |  |
+| prometheus.podMonitor.additionalLabels | object | `{}` |  |
+| prometheus.podMonitor.annotations | object | `{}` |  |
+| prometheus.podMonitor.enabled | bool | `false` |  |
+| prometheus.podMonitor.interval | string | `nil` |  |
+| prometheus.podMonitor.jobLabel | string | `"app.kubernetes.io/name"` |  |
+| prometheus.podMonitor.metricRelabelings | list | `[]` |  |
+| prometheus.podMonitor.relabelings | list | `[]` |  |
+| prometheus.prometheusRule.additionalLabels | object | `{}` |  |
+| prometheus.prometheusRule.addressPoolExhausted.enabled | bool | `true` |  |
+| prometheus.prometheusRule.addressPoolExhausted.labels.severity | string | `"alert"` |  |
+| prometheus.prometheusRule.addressPoolUsage.enabled | bool | `true` |  |
+| prometheus.prometheusRule.addressPoolUsage.thresholds[0].labels.severity | string | `"warning"` |  |
+| prometheus.prometheusRule.addressPoolUsage.thresholds[0].percent | int | `75` |  |
+| prometheus.prometheusRule.addressPoolUsage.thresholds[1].labels.severity | string | `"warning"` |  |
+| prometheus.prometheusRule.addressPoolUsage.thresholds[1].percent | int | `85` |  |
+| prometheus.prometheusRule.addressPoolUsage.thresholds[2].labels.severity | string | `"alert"` |  |
+| prometheus.prometheusRule.addressPoolUsage.thresholds[2].percent | int | `95` |  |
+| prometheus.prometheusRule.annotations | object | `{}` |  |
+| prometheus.prometheusRule.bgpSessionDown.enabled | bool | `true` |  |
+| prometheus.prometheusRule.bgpSessionDown.labels.severity | string | `"alert"` |  |
+| prometheus.prometheusRule.configNotLoaded.enabled | bool | `true` |  |
+| prometheus.prometheusRule.configNotLoaded.labels.severity | string | `"warning"` |  |
+| prometheus.prometheusRule.enabled | bool | `false` |  |
+| prometheus.prometheusRule.extraAlerts | list | `[]` |  |
+| prometheus.prometheusRule.staleConfig.enabled | bool | `true` |  |
+| prometheus.prometheusRule.staleConfig.labels.severity | string | `"warning"` |  |
+| prometheus.rbacPrometheus | bool | `true` |  |
+| prometheus.rbacProxy.repository | string | `"gcr.io/kubebuilder/kube-rbac-proxy"` |  |
+| prometheus.rbacProxy.tag | string | `"v0.12.0"` |  |
+| prometheus.scrapeAnnotations | bool | `false` |  |
+| prometheus.serviceAccount | string | `""` |  |
+| prometheus.serviceMonitor.controller.additionalLabels | object | `{}` |  |
+| prometheus.serviceMonitor.controller.annotations | object | `{}` |  |
+| prometheus.serviceMonitor.controller.tlsConfig.insecureSkipVerify | bool | `true` |  |
+| prometheus.serviceMonitor.enabled | bool | `false` |  |
+| prometheus.serviceMonitor.interval | string | `nil` |  |
+| prometheus.serviceMonitor.jobLabel | string | `"app.kubernetes.io/name"` |  |
+| prometheus.serviceMonitor.metricRelabelings | list | `[]` |  |
+| prometheus.serviceMonitor.relabelings | list | `[]` |  |
+| prometheus.serviceMonitor.speaker.additionalLabels | object | `{}` |  |
+| prometheus.serviceMonitor.speaker.annotations | object | `{}` |  |
+| prometheus.serviceMonitor.speaker.tlsConfig.insecureSkipVerify | bool | `true` |  |
+| prometheus.speakerMetricsTLSSecret | string | `""` |  |
+| rbac.create | bool | `true` |  |
+| speaker.affinity | object | `{}` |  |
+| speaker.enabled | bool | `true` |  |
+| speaker.frr.enabled | bool | `false` |  |
+| speaker.frr.image.pullPolicy | string | `nil` |  |
+| speaker.frr.image.repository | string | `"frrouting/frr"` |  |
+| speaker.frr.image.tag | string | `"v7.5.1"` |  |
+| speaker.frr.metricsPort | int | `7473` |  |
+| speaker.frr.resources | object | `{}` |  |
+| speaker.frrMetrics.resources | object | `{}` |  |
+| speaker.image.pullPolicy | string | `nil` |  |
+| speaker.image.repository | string | `"quay.io/metallb/speaker"` |  |
+| speaker.image.tag | string | `nil` |  |
+| speaker.livenessProbe.enabled | bool | `true` |  |
+| speaker.livenessProbe.failureThreshold | int | `3` |  |
+| speaker.livenessProbe.initialDelaySeconds | int | `10` |  |
+| speaker.livenessProbe.periodSeconds | int | `10` |  |
+| speaker.livenessProbe.successThreshold | int | `1` |  |
+| speaker.livenessProbe.timeoutSeconds | int | `1` |  |
+| speaker.logLevel | string | `"info"` | Speaker log level. Must be one of: `all`, `debug`, `info`, `warn`, `error` or `none` |
+| speaker.memberlist.enabled | bool | `true` |  |
+| speaker.memberlist.mlBindPort | int | `7946` |  |
+| speaker.nodeSelector | object | `{}` |  |
+| speaker.podAnnotations | object | `{}` |  |
+| speaker.priorityClassName | string | `""` |  |
+| speaker.readinessProbe.enabled | bool | `true` |  |
+| speaker.readinessProbe.failureThreshold | int | `3` |  |
+| speaker.readinessProbe.initialDelaySeconds | int | `10` |  |
+| speaker.readinessProbe.periodSeconds | int | `10` |  |
+| speaker.readinessProbe.successThreshold | int | `1` |  |
+| speaker.readinessProbe.timeoutSeconds | int | `1` |  |
+| speaker.reloader.resources | object | `{}` |  |
+| speaker.resources | object | `{}` |  |
+| speaker.runtimeClassName | string | `""` |  |
+| speaker.serviceAccount.annotations | object | `{}` |  |
+| speaker.serviceAccount.create | bool | `true` |  |
+| speaker.serviceAccount.name | string | `""` |  |
+| speaker.tolerateMaster | bool | `true` |  |
+| speaker.tolerations | list | `[]` |  |
+| speaker.updateStrategy.type | string | `"RollingUpdate"` |  |
+
+----------------------------------------------
+Autogenerated from chart metadata using [helm-docs v1.10.0](https://github.com/norwoodj/helm-docs/releases/v1.10.0)
diff --git a/charts/metallb/templates/manifest.yaml b/charts/metallb-0.13.7/templates/manifest.yaml
similarity index 100%
rename from charts/metallb/templates/manifest.yaml
rename to charts/metallb-0.13.7/templates/manifest.yaml
diff --git a/charts/metallb-0.13.7/values.yaml b/charts/metallb-0.13.7/values.yaml
new file mode 100644
index 0000000..2b990b7
--- /dev/null
+++ b/charts/metallb-0.13.7/values.yaml
@@ -0,0 +1,12 @@
+controller:
+  image:
+    repository: quay.io/metallb/controller
+    tag:
+    pullPolicy:
+  logLevel: info
+speaker:
+  image:
+    repository: quay.io/metallb/speaker
+    tag:
+    pullPolicy:
+  logLevel: info
diff --git a/charts/metallb/Chart.lock b/charts/metallb/Chart.lock
new file mode 100644
index 0000000..425c50f
--- /dev/null
+++ b/charts/metallb/Chart.lock
@@ -0,0 +1,6 @@
+dependencies:
+- name: crds
+  repository: ""
+  version: 0.13.12
+digest: sha256:bc3d2abdac552d6a886bd1d533eef9a432e5809a0dda4a85c7de4fdf2094cdb0
+generated: "2023-10-20T16:56:55.333731157+02:00"
diff --git a/charts/metallb/Chart.yaml b/charts/metallb/Chart.yaml
index 76e774d..6e964d7 100644
--- a/charts/metallb/Chart.yaml
+++ b/charts/metallb/Chart.yaml
@@ -1,11 +1,17 @@
 apiVersion: v2
-appVersion: v0.13.7
+appVersion: v0.13.12
+dependencies:
+- condition: crds.enabled
+  name: crds
+  repository: ""
+  version: 0.13.12
 description: A network load-balancer implementation for Kubernetes using standard
   routing protocols
 home: https://metallb.universe.tf
 icon: https://metallb.universe.tf/images/logo/metallb-white.png
+kubeVersion: '>= 1.19.0-0'
 name: metallb
 sources:
 - https://github.com/metallb/metallb
 type: application
-version: 0.13.7
+version: 0.13.12
diff --git a/charts/metallb/README.md b/charts/metallb/README.md
index 25cb5d4..11bbe7d 100644
--- a/charts/metallb/README.md
+++ b/charts/metallb/README.md
@@ -12,6 +12,8 @@
 
 ## Requirements
 
+Kubernetes: `>= 1.19.0-0`
+
 | Repository | Name | Version |
 |------------|------|---------|
 |  | crds | 0.0.0 |
@@ -25,6 +27,7 @@
 | controller.image.pullPolicy | string | `nil` |  |
 | controller.image.repository | string | `"quay.io/metallb/controller"` |  |
 | controller.image.tag | string | `nil` |  |
+| controller.labels | object | `{}` |  |
 | controller.livenessProbe.enabled | bool | `true` |  |
 | controller.livenessProbe.failureThreshold | int | `3` |  |
 | controller.livenessProbe.initialDelaySeconds | int | `10` |  |
@@ -87,6 +90,7 @@
 | prometheus.prometheusRule.staleConfig.enabled | bool | `true` |  |
 | prometheus.prometheusRule.staleConfig.labels.severity | string | `"warning"` |  |
 | prometheus.rbacPrometheus | bool | `true` |  |
+| prometheus.rbacProxy.pullPolicy | string | `nil` |  |
 | prometheus.rbacProxy.repository | string | `"gcr.io/kubebuilder/kube-rbac-proxy"` |  |
 | prometheus.rbacProxy.tag | string | `"v0.12.0"` |  |
 | prometheus.scrapeAnnotations | bool | `false` |  |
@@ -106,16 +110,18 @@
 | rbac.create | bool | `true` |  |
 | speaker.affinity | object | `{}` |  |
 | speaker.enabled | bool | `true` |  |
-| speaker.frr.enabled | bool | `false` |  |
+| speaker.excludeInterfaces.enabled | bool | `true` |  |
+| speaker.frr.enabled | bool | `true` |  |
 | speaker.frr.image.pullPolicy | string | `nil` |  |
-| speaker.frr.image.repository | string | `"frrouting/frr"` |  |
-| speaker.frr.image.tag | string | `"v7.5.1"` |  |
+| speaker.frr.image.repository | string | `"quay.io/frrouting/frr"` |  |
+| speaker.frr.image.tag | string | `"8.5.2"` |  |
 | speaker.frr.metricsPort | int | `7473` |  |
 | speaker.frr.resources | object | `{}` |  |
 | speaker.frrMetrics.resources | object | `{}` |  |
 | speaker.image.pullPolicy | string | `nil` |  |
 | speaker.image.repository | string | `"quay.io/metallb/speaker"` |  |
 | speaker.image.tag | string | `nil` |  |
+| speaker.labels | object | `{}` |  |
 | speaker.livenessProbe.enabled | bool | `true` |  |
 | speaker.livenessProbe.failureThreshold | int | `3` |  |
 | speaker.livenessProbe.initialDelaySeconds | int | `10` |  |
@@ -125,6 +131,7 @@
 | speaker.logLevel | string | `"info"` | Speaker log level. Must be one of: `all`, `debug`, `info`, `warn`, `error` or `none` |
 | speaker.memberlist.enabled | bool | `true` |  |
 | speaker.memberlist.mlBindPort | int | `7946` |  |
+| speaker.memberlist.mlSecretKeyPath | string | `"/etc/ml_secret_key"` |  |
 | speaker.nodeSelector | object | `{}` |  |
 | speaker.podAnnotations | object | `{}` |  |
 | speaker.priorityClassName | string | `""` |  |
@@ -140,6 +147,9 @@
 | speaker.serviceAccount.annotations | object | `{}` |  |
 | speaker.serviceAccount.create | bool | `true` |  |
 | speaker.serviceAccount.name | string | `""` |  |
+| speaker.startupProbe.enabled | bool | `true` |  |
+| speaker.startupProbe.failureThreshold | int | `30` |  |
+| speaker.startupProbe.periodSeconds | int | `5` |  |
 | speaker.tolerateMaster | bool | `true` |  |
 | speaker.tolerations | list | `[]` |  |
 | speaker.updateStrategy.type | string | `"RollingUpdate"` |  |
diff --git a/charts/metallb/charts/crds/.helmignore b/charts/metallb/charts/crds/.helmignore
new file mode 100644
index 0000000..0e8a0eb
--- /dev/null
+++ b/charts/metallb/charts/crds/.helmignore
@@ -0,0 +1,23 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*.orig
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
+.vscode/
diff --git a/charts/metallb/charts/crds/Chart.yaml b/charts/metallb/charts/crds/Chart.yaml
new file mode 100644
index 0000000..255ac2b
--- /dev/null
+++ b/charts/metallb/charts/crds/Chart.yaml
@@ -0,0 +1,10 @@
+apiVersion: v2
+appVersion: v0.13.12
+description: MetalLB CRDs
+home: https://metallb.universe.tf
+icon: https://metallb.universe.tf/images/logo/metallb-white.png
+name: crds
+sources:
+- https://github.com/metallb/metallb
+type: application
+version: 0.13.12
diff --git a/charts/metallb/charts/crds/README.md b/charts/metallb/charts/crds/README.md
new file mode 100644
index 0000000..15bf8a7
--- /dev/null
+++ b/charts/metallb/charts/crds/README.md
@@ -0,0 +1,14 @@
+# crds
+
+![Version: 0.0.0](https://img.shields.io/badge/Version-0.0.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v0.0.0](https://img.shields.io/badge/AppVersion-v0.0.0-informational?style=flat-square)
+
+MetalLB CRDs
+
+**Homepage:** <https://metallb.universe.tf>
+
+## Source Code
+
+* <https://github.com/metallb/metallb>
+
+----------------------------------------------
+Autogenerated from chart metadata using [helm-docs v1.10.0](https://github.com/norwoodj/helm-docs/releases/v1.10.0)
diff --git a/charts/metallb/charts/crds/templates/crds.yaml b/charts/metallb/charts/crds/templates/crds.yaml
new file mode 100644
index 0000000..9b415ac
--- /dev/null
+++ b/charts/metallb/charts/crds/templates/crds.yaml
@@ -0,0 +1,1233 @@
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  name: addresspools.metallb.io
+spec:
+  group: metallb.io
+  names:
+    kind: AddressPool
+    listKind: AddressPoolList
+    plural: addresspools
+    singular: addresspool
+  scope: Namespaced
+  conversion:
+    strategy: Webhook
+    webhook:
+      conversionReviewVersions: ["v1alpha1", "v1beta1"]
+      clientConfig:
+        # this is a valid pem format, otherwise the apiserver will reject the deletion of the crds
+        # with "unable to parse bytes as PEM block", The controller will patch it with the right content after it starts
+        caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlGWlRDQ0EwMmdBd0lCQWdJVU5GRW1XcTM3MVpKdGkrMmlSQzk1WmpBV1MxZ3dEUVlKS29aSWh2Y05BUUVMDQpCUUF3UWpFTE1Ba0dBMVVFQmhNQ1dGZ3hGVEFUQmdOVkJBY01ERVJsWm1GMWJIUWdRMmwwZVRFY01Cb0dBMVVFDQpDZ3dUUkdWbVlYVnNkQ0JEYjIxd1lXNTVJRXgwWkRBZUZ3MHlNakEzTVRrd09UTXlNek5hRncweU1qQTRNVGd3DQpPVE15TXpOYU1FSXhDekFKQmdOVkJBWVRBbGhZTVJVd0V3WURWUVFIREF4RVpXWmhkV3gwSUVOcGRIa3hIREFhDQpCZ05WQkFvTUUwUmxabUYxYkhRZ1EyOXRjR0Z1ZVNCTWRHUXdnZ0lpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElDDQpEd0F3Z2dJS0FvSUNBUUNxVFpxMWZRcC9vYkdlenhES0o3OVB3Ny94azJwellualNzMlkzb1ZYSm5sRmM4YjVlDQpma2ZZQnY2bndscW1keW5PL2phWFBaQmRQSS82aFdOUDBkdVhadEtWU0NCUUpyZzEyOGNXb3F0MGNTN3pLb1VpDQpvcU1tQ0QvRXVBeFFNZjhRZDF2c1gvVllkZ0poVTZBRXJLZEpIaXpFOUJtUkNkTDBGMW1OVW55Rk82UnRtWFZUDQpidkxsTDVYeTc2R0FaQVBLOFB4aVlDa0NtbDdxN0VnTWNiOXlLWldCYmlxQ3VkTXE5TGJLNmdKNzF6YkZnSXV4DQo1L1pXK2JraTB2RlplWk9ZODUxb1psckFUNzJvMDI4NHNTWW9uN0pHZVZkY3NoUnh5R1VpSFpSTzdkaXZVTDVTDQpmM2JmSDFYbWY1ZDQzT0NWTWRuUUV2NWVaOG8zeWVLa3ZrbkZQUGVJMU9BbjdGbDlFRVNNR2dhOGFaSG1URSttDQpsLzlMSmdDYjBnQmtPT0M0WnV4bWh2aERKV1EzWnJCS3pMQlNUZXN0NWlLNVlwcXRWVVk2THRyRW9FelVTK1lsDQpwWndXY2VQWHlHeHM5ZURsR3lNVmQraW15Y3NTU1UvVno2Mmx6MnZCS21NTXBkYldDQWhud0RsRTVqU2dyMjRRDQp0eGNXLys2N3d5KzhuQlI3UXdqVTFITndVRjBzeERWdEwrZ1NHVERnSEVZSlhZelYvT05zMy94TkpoVFNPSkxNDQpoeXNVdyttaGdackdhbUdXcHVIVU1DUitvTWJzMTc1UkcrQjJnUFFHVytPTjJnUTRyOXN2b0ZBNHBBQm8xd1dLDQpRYjRhY3pmeVVscElBOVFoSmFsZEY3S3dPSHVlV3gwRUNrNXg0T2tvVDBvWVp0dzFiR0JjRGtaSmF3SURBUUFCDQpvMU13VVRBZEJnTlZIUTRFRmdRVW90UlNIUm9IWTEyRFZ4R0NCdEhpb1g2ZmVFQXdId1lEVlIwakJCZ3dGb0FVDQpvdFJTSFJvSFkxMkRWeEdDQnRIaW9YNmZlRUF3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFOQmdrcWhraUc5dzBCDQpBUXNGQUFPQ0FnRUFSbkpsWWRjMTFHd0VxWnh6RDF2R3BDR2pDN2VWTlQ3aVY1d3IybXlybHdPYi9aUWFEa0xYDQpvVStaOVVXT1VlSXJTdzUydDdmQUpvVVAwSm5iYkMveVIrU1lqUGhvUXNiVHduOTc2ZldBWTduM3FMOXhCd1Y0DQphek41OXNjeUp0dlhMeUtOL2N5ak1ReDRLajBIMFg0bWJ6bzVZNUtzWWtYVU0vOEFPdWZMcEd0S1NGVGgrSEFDDQpab1Q5YnZHS25adnNHd0tYZFF0Wnh0akhaUjVqK3U3ZGtQOTJBT051RFNabS8rWVV4b2tBK09JbzdSR3BwSHNXDQo1ZTdNY0FTVXRtb1FORXd6dVFoVkJaRWQ1OGtKYjUrV0VWbGNzanlXNnRTbzErZ25tTWNqR1BsMWgxR2hVbjV4DQpFY0lWRnBIWXM5YWo1NmpBSjk1MVQvZjhMaWxmTlVnanBLQ0c1bnl0SUt3emxhOHNtdGlPdm1UNEpYbXBwSkI2DQo4bmdHRVluVjUrUTYwWFJ2OEhSSGp1VG9CRHVhaERrVDA2R1JGODU1d09FR2V4bkZpMXZYWUxLVllWb1V2MXRKDQo4dVdUR1pwNllDSVJldlBqbzg5ZytWTlJSaVFYUThJd0dybXE5c0RoVTlqTjA0SjdVL1RvRDFpNHE3VnlsRUc5DQorV1VGNkNLaEdBeTJIaEhwVncyTGFoOS9lUzdZMUZ1YURrWmhPZG1laG1BOCtqdHNZamJadnR5Mm1SWlF0UUZzDQpUU1VUUjREbUR2bVVPRVRmeStpRHdzK2RkWXVNTnJGeVVYV2dkMnpBQU4ydVl1UHFGY2pRcFNPODFzVTJTU3R3DQoxVzAyeUtYOGJEYmZFdjBzbUh3UzliQnFlSGo5NEM1Mjg0YXpsdTBmaUdpTm1OUEM4ckJLRmhBPQ0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==
+        service:
+          namespace: {{ .Release.Namespace }}
+          name: metallb-webhook-service
+          path: /convert
+  versions:
+  - deprecated: true
+    deprecationWarning: metallb.io v1alpha1 AddressPool is deprecated
+    name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: AddressPool is the Schema for the addresspools API.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: AddressPoolSpec defines the desired state of AddressPool.
+            properties:
+              addresses:
+                description: A list of IP address ranges over which MetalLB has authority.
+                  You can list multiple ranges in a single pool, they will all share
+                  the same settings. Each range can be either a CIDR prefix, or an
+                  explicit start-end range of IPs.
+                items:
+                  type: string
+                type: array
+              autoAssign:
+                default: true
+                description: AutoAssign flag used to prevent MetallB from automatic
+                  allocation for a pool.
+                type: boolean
+              bgpAdvertisements:
+                description: When an IP is allocated from this pool, how should it
+                  be translated into BGP announcements?
+                items:
+                  properties:
+                    aggregationLength:
+                      default: 32
+                      description: The aggregation-length advertisement option lets
+                        you “roll up” the /32s into a larger prefix.
+                      format: int32
+                      minimum: 1
+                      type: integer
+                    aggregationLengthV6:
+                      default: 128
+                      description: Optional, defaults to 128 (i.e. no aggregation)
+                        if not specified.
+                      format: int32
+                      type: integer
+                    communities:
+                      description: BGP communities
+                      items:
+                        type: string
+                      type: array
+                    localPref:
+                      description: BGP LOCAL_PREF attribute which is used by BGP best
+                        path algorithm, Path with higher localpref is preferred over
+                        one with lower localpref.
+                      format: int32
+                      type: integer
+                  type: object
+                type: array
+              protocol:
+                description: Protocol can be used to select how the announcement is
+                  done.
+                enum:
+                - layer2
+                - bgp
+                type: string
+            required:
+            - addresses
+            - protocol
+            type: object
+          status:
+            description: AddressPoolStatus defines the observed state of AddressPool.
+            type: object
+        required:
+        - spec
+        type: object
+    served: true
+    storage: false
+    subresources:
+      status: {}
+  - deprecated: true
+    deprecationWarning: metallb.io v1beta1 AddressPool is deprecated, consider using
+      IPAddressPool
+    name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: AddressPool represents a pool of IP addresses that can be allocated
+          to LoadBalancer services. AddressPool is deprecated and being replaced by
+          IPAddressPool.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: AddressPoolSpec defines the desired state of AddressPool.
+            properties:
+              addresses:
+                description: A list of IP address ranges over which MetalLB has authority.
+                  You can list multiple ranges in a single pool, they will all share
+                  the same settings. Each range can be either a CIDR prefix, or an
+                  explicit start-end range of IPs.
+                items:
+                  type: string
+                type: array
+              autoAssign:
+                default: true
+                description: AutoAssign flag used to prevent MetallB from automatic
+                  allocation for a pool.
+                type: boolean
+              bgpAdvertisements:
+                description: Drives how an IP allocated from this pool should translated
+                  into BGP announcements.
+                items:
+                  properties:
+                    aggregationLength:
+                      default: 32
+                      description: The aggregation-length advertisement option lets
+                        you “roll up” the /32s into a larger prefix.
+                      format: int32
+                      minimum: 1
+                      type: integer
+                    aggregationLengthV6:
+                      default: 128
+                      description: Optional, defaults to 128 (i.e. no aggregation)
+                        if not specified.
+                      format: int32
+                      type: integer
+                    communities:
+                      description: BGP communities to be associated with the given
+                        advertisement.
+                      items:
+                        type: string
+                      type: array
+                    localPref:
+                      description: BGP LOCAL_PREF attribute which is used by BGP best
+                        path algorithm, Path with higher localpref is preferred over
+                        one with lower localpref.
+                      format: int32
+                      type: integer
+                  type: object
+                type: array
+              protocol:
+                description: Protocol can be used to select how the announcement is
+                  done.
+                enum:
+                - layer2
+                - bgp
+                type: string
+            required:
+            - addresses
+            - protocol
+            type: object
+          status:
+            description: AddressPoolStatus defines the observed state of AddressPool.
+            type: object
+        required:
+        - spec
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  name: bfdprofiles.metallb.io
+spec:
+  group: metallb.io
+  names:
+    kind: BFDProfile
+    listKind: BFDProfileList
+    plural: bfdprofiles
+    singular: bfdprofile
+  scope: Namespaced
+  versions:
+  - name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: BFDProfile represents the settings of the bfd session that can
+          be optionally associated with a BGP session.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: BFDProfileSpec defines the desired state of BFDProfile.
+            properties:
+              detectMultiplier:
+                description: Configures the detection multiplier to determine packet
+                  loss. The remote transmission interval will be multiplied by this
+                  value to determine the connection loss detection timer.
+                format: int32
+                maximum: 255
+                minimum: 2
+                type: integer
+              echoInterval:
+                description: Configures the minimal echo receive transmission interval
+                  that this system is capable of handling in milliseconds. Defaults
+                  to 50ms
+                format: int32
+                maximum: 60000
+                minimum: 10
+                type: integer
+              echoMode:
+                description: Enables or disables the echo transmission mode. This
+                  mode is disabled by default, and not supported on multi hops setups.
+                type: boolean
+              minimumTtl:
+                description: 'For multi hop sessions only: configure the minimum expected
+                  TTL for an incoming BFD control packet.'
+                format: int32
+                maximum: 254
+                minimum: 1
+                type: integer
+              passiveMode:
+                description: 'Mark session as passive: a passive session will not
+                  attempt to start the connection and will wait for control packets
+                  from peer before it begins replying.'
+                type: boolean
+              receiveInterval:
+                description: The minimum interval that this system is capable of receiving
+                  control packets in milliseconds. Defaults to 300ms.
+                format: int32
+                maximum: 60000
+                minimum: 10
+                type: integer
+              transmitInterval:
+                description: The minimum transmission interval (less jitter) that
+                  this system wants to use to send BFD control packets in milliseconds.
+                  Defaults to 300ms
+                format: int32
+                maximum: 60000
+                minimum: 10
+                type: integer
+            type: object
+          status:
+            description: BFDProfileStatus defines the observed state of BFDProfile.
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  name: bgpadvertisements.metallb.io
+spec:
+  group: metallb.io
+  names:
+    kind: BGPAdvertisement
+    listKind: BGPAdvertisementList
+    plural: bgpadvertisements
+    singular: bgpadvertisement
+  scope: Namespaced
+  versions:
+  - name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: BGPAdvertisement allows to advertise the IPs coming from the
+          selected IPAddressPools via BGP, setting the parameters of the BGP Advertisement.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: BGPAdvertisementSpec defines the desired state of BGPAdvertisement.
+            properties:
+              aggregationLength:
+                default: 32
+                description: The aggregation-length advertisement option lets you
+                  “roll up” the /32s into a larger prefix. Defaults to 32. Works for
+                  IPv4 addresses.
+                format: int32
+                minimum: 1
+                type: integer
+              aggregationLengthV6:
+                default: 128
+                description: The aggregation-length advertisement option lets you
+                  “roll up” the /128s into a larger prefix. Defaults to 128. Works
+                  for IPv6 addresses.
+                format: int32
+                type: integer
+              communities:
+                description: The BGP communities to be associated with the announcement.
+                  Each item can be a community of the form 1234:1234 or the name of
+                  an alias defined in the Community CRD.
+                items:
+                  type: string
+                type: array
+              ipAddressPoolSelectors:
+                description: A selector for the IPAddressPools which would get advertised
+                  via this advertisement. If no IPAddressPool is selected by this
+                  or by the list, the advertisement is applied to all the IPAddressPools.
+                items:
+                  description: A label selector is a label query over a set of resources.
+                    The result of matchLabels and matchExpressions are ANDed. An empty
+                    label selector matches all objects. A null label selector matches
+                    no objects.
+                  properties:
+                    matchExpressions:
+                      description: matchExpressions is a list of label selector requirements.
+                        The requirements are ANDed.
+                      items:
+                        description: A label selector requirement is a selector that
+                          contains values, a key, and an operator that relates the
+                          key and values.
+                        properties:
+                          key:
+                            description: key is the label key that the selector applies
+                              to.
+                            type: string
+                          operator:
+                            description: operator represents a key's relationship
+                              to a set of values. Valid operators are In, NotIn, Exists
+                              and DoesNotExist.
+                            type: string
+                          values:
+                            description: values is an array of string values. If the
+                              operator is In or NotIn, the values array must be non-empty.
+                              If the operator is Exists or DoesNotExist, the values
+                              array must be empty. This array is replaced during a
+                              strategic merge patch.
+                            items:
+                              type: string
+                            type: array
+                        required:
+                        - key
+                        - operator
+                        type: object
+                      type: array
+                    matchLabels:
+                      additionalProperties:
+                        type: string
+                      description: matchLabels is a map of {key,value} pairs. A single
+                        {key,value} in the matchLabels map is equivalent to an element
+                        of matchExpressions, whose key field is "key", the operator
+                        is "In", and the values array contains only "value". The requirements
+                        are ANDed.
+                      type: object
+                  type: object
+                type: array
+              ipAddressPools:
+                description: The list of IPAddressPools to advertise via this advertisement,
+                  selected by name.
+                items:
+                  type: string
+                type: array
+              localPref:
+                description: The BGP LOCAL_PREF attribute which is used by BGP best
+                  path algorithm, Path with higher localpref is preferred over one
+                  with lower localpref.
+                format: int32
+                type: integer
+              nodeSelectors:
+                description: NodeSelectors allows to limit the nodes to announce as
+                  next hops for the LoadBalancer IP. When empty, all the nodes having  are
+                  announced as next hops.
+                items:
+                  description: A label selector is a label query over a set of resources.
+                    The result of matchLabels and matchExpressions are ANDed. An empty
+                    label selector matches all objects. A null label selector matches
+                    no objects.
+                  properties:
+                    matchExpressions:
+                      description: matchExpressions is a list of label selector requirements.
+                        The requirements are ANDed.
+                      items:
+                        description: A label selector requirement is a selector that
+                          contains values, a key, and an operator that relates the
+                          key and values.
+                        properties:
+                          key:
+                            description: key is the label key that the selector applies
+                              to.
+                            type: string
+                          operator:
+                            description: operator represents a key's relationship
+                              to a set of values. Valid operators are In, NotIn, Exists
+                              and DoesNotExist.
+                            type: string
+                          values:
+                            description: values is an array of string values. If the
+                              operator is In or NotIn, the values array must be non-empty.
+                              If the operator is Exists or DoesNotExist, the values
+                              array must be empty. This array is replaced during a
+                              strategic merge patch.
+                            items:
+                              type: string
+                            type: array
+                        required:
+                        - key
+                        - operator
+                        type: object
+                      type: array
+                    matchLabels:
+                      additionalProperties:
+                        type: string
+                      description: matchLabels is a map of {key,value} pairs. A single
+                        {key,value} in the matchLabels map is equivalent to an element
+                        of matchExpressions, whose key field is "key", the operator
+                        is "In", and the values array contains only "value". The requirements
+                        are ANDed.
+                      type: object
+                  type: object
+                type: array
+              peers:
+                description: Peers limits the bgppeer to advertise the ips of the
+                  selected pools to. When empty, the loadbalancer IP is announced
+                  to all the BGPPeers configured.
+                items:
+                  type: string
+                type: array
+            type: object
+          status:
+            description: BGPAdvertisementStatus defines the observed state of BGPAdvertisement.
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  name: bgppeers.metallb.io
+spec:
+  group: metallb.io
+  names:
+    kind: BGPPeer
+    listKind: BGPPeerList
+    plural: bgppeers
+    singular: bgppeer
+  scope: Namespaced
+  conversion:
+    strategy: Webhook
+    webhook:
+      conversionReviewVersions: ["v1beta1", "v1beta2"]
+      clientConfig:
+        # this is a valid pem format, otherwise the apiserver will reject the deletion of the crds
+        # with "unable to parse bytes as PEM block", The controller will patch it with the right content after it starts
+        caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlGWlRDQ0EwMmdBd0lCQWdJVU5GRW1XcTM3MVpKdGkrMmlSQzk1WmpBV1MxZ3dEUVlKS29aSWh2Y05BUUVMDQpCUUF3UWpFTE1Ba0dBMVVFQmhNQ1dGZ3hGVEFUQmdOVkJBY01ERVJsWm1GMWJIUWdRMmwwZVRFY01Cb0dBMVVFDQpDZ3dUUkdWbVlYVnNkQ0JEYjIxd1lXNTVJRXgwWkRBZUZ3MHlNakEzTVRrd09UTXlNek5hRncweU1qQTRNVGd3DQpPVE15TXpOYU1FSXhDekFKQmdOVkJBWVRBbGhZTVJVd0V3WURWUVFIREF4RVpXWmhkV3gwSUVOcGRIa3hIREFhDQpCZ05WQkFvTUUwUmxabUYxYkhRZ1EyOXRjR0Z1ZVNCTWRHUXdnZ0lpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElDDQpEd0F3Z2dJS0FvSUNBUUNxVFpxMWZRcC9vYkdlenhES0o3OVB3Ny94azJwellualNzMlkzb1ZYSm5sRmM4YjVlDQpma2ZZQnY2bndscW1keW5PL2phWFBaQmRQSS82aFdOUDBkdVhadEtWU0NCUUpyZzEyOGNXb3F0MGNTN3pLb1VpDQpvcU1tQ0QvRXVBeFFNZjhRZDF2c1gvVllkZ0poVTZBRXJLZEpIaXpFOUJtUkNkTDBGMW1OVW55Rk82UnRtWFZUDQpidkxsTDVYeTc2R0FaQVBLOFB4aVlDa0NtbDdxN0VnTWNiOXlLWldCYmlxQ3VkTXE5TGJLNmdKNzF6YkZnSXV4DQo1L1pXK2JraTB2RlplWk9ZODUxb1psckFUNzJvMDI4NHNTWW9uN0pHZVZkY3NoUnh5R1VpSFpSTzdkaXZVTDVTDQpmM2JmSDFYbWY1ZDQzT0NWTWRuUUV2NWVaOG8zeWVLa3ZrbkZQUGVJMU9BbjdGbDlFRVNNR2dhOGFaSG1URSttDQpsLzlMSmdDYjBnQmtPT0M0WnV4bWh2aERKV1EzWnJCS3pMQlNUZXN0NWlLNVlwcXRWVVk2THRyRW9FelVTK1lsDQpwWndXY2VQWHlHeHM5ZURsR3lNVmQraW15Y3NTU1UvVno2Mmx6MnZCS21NTXBkYldDQWhud0RsRTVqU2dyMjRRDQp0eGNXLys2N3d5KzhuQlI3UXdqVTFITndVRjBzeERWdEwrZ1NHVERnSEVZSlhZelYvT05zMy94TkpoVFNPSkxNDQpoeXNVdyttaGdackdhbUdXcHVIVU1DUitvTWJzMTc1UkcrQjJnUFFHVytPTjJnUTRyOXN2b0ZBNHBBQm8xd1dLDQpRYjRhY3pmeVVscElBOVFoSmFsZEY3S3dPSHVlV3gwRUNrNXg0T2tvVDBvWVp0dzFiR0JjRGtaSmF3SURBUUFCDQpvMU13VVRBZEJnTlZIUTRFRmdRVW90UlNIUm9IWTEyRFZ4R0NCdEhpb1g2ZmVFQXdId1lEVlIwakJCZ3dGb0FVDQpvdFJTSFJvSFkxMkRWeEdDQnRIaW9YNmZlRUF3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFOQmdrcWhraUc5dzBCDQpBUXNGQUFPQ0FnRUFSbkpsWWRjMTFHd0VxWnh6RDF2R3BDR2pDN2VWTlQ3aVY1d3IybXlybHdPYi9aUWFEa0xYDQpvVStaOVVXT1VlSXJTdzUydDdmQUpvVVAwSm5iYkMveVIrU1lqUGhvUXNiVHduOTc2ZldBWTduM3FMOXhCd1Y0DQphek41OXNjeUp0dlhMeUtOL2N5ak1ReDRLajBIMFg0bWJ6bzVZNUtzWWtYVU0vOEFPdWZMcEd0S1NGVGgrSEFDDQpab1Q5YnZHS25adnNHd0tYZFF0Wnh0akhaUjVqK3U3ZGtQOTJBT051RFNabS8rWVV4b2tBK09JbzdSR3BwSHNXDQo1ZTdNY0FTVXRtb1FORXd6dVFoVkJaRWQ1OGtKYjUrV0VWbGNzanlXNnRTbzErZ25tTWNqR1BsMWgxR2hVbjV4DQpFY0lWRnBIWXM5YWo1NmpBSjk1MVQvZjhMaWxmTlVnanBLQ0c1bnl0SUt3emxhOHNtdGlPdm1UNEpYbXBwSkI2DQo4bmdHRVluVjUrUTYwWFJ2OEhSSGp1VG9CRHVhaERrVDA2R1JGODU1d09FR2V4bkZpMXZYWUxLVllWb1V2MXRKDQo4dVdUR1pwNllDSVJldlBqbzg5ZytWTlJSaVFYUThJd0dybXE5c0RoVTlqTjA0SjdVL1RvRDFpNHE3VnlsRUc5DQorV1VGNkNLaEdBeTJIaEhwVncyTGFoOS9lUzdZMUZ1YURrWmhPZG1laG1BOCtqdHNZamJadnR5Mm1SWlF0UUZzDQpUU1VUUjREbUR2bVVPRVRmeStpRHdzK2RkWXVNTnJGeVVYV2dkMnpBQU4ydVl1UHFGY2pRcFNPODFzVTJTU3R3DQoxVzAyeUtYOGJEYmZFdjBzbUh3UzliQnFlSGo5NEM1Mjg0YXpsdTBmaUdpTm1OUEM4ckJLRmhBPQ0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==
+        service:
+          namespace: {{ .Release.Namespace }}
+          name: metallb-webhook-service
+          path: /convert
+  versions:
+  - name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: BGPPeer is the Schema for the peers API.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: BGPPeerSpec defines the desired state of Peer.
+            properties:
+              bfdProfile:
+                type: string
+              ebgpMultiHop:
+                description: EBGP peer is multi-hops away
+                type: boolean
+              holdTime:
+                description: Requested BGP hold time, per RFC4271.
+                type: string
+              keepaliveTime:
+                description: Requested BGP keepalive time, per RFC4271.
+                type: string
+              myASN:
+                description: AS number to use for the local end of the session.
+                format: int32
+                maximum: 4294967295
+                minimum: 0
+                type: integer
+              nodeSelectors:
+                description: Only connect to this peer on nodes that match one of
+                  these selectors.
+                items:
+                  properties:
+                    matchExpressions:
+                      items:
+                        properties:
+                          key:
+                            type: string
+                          operator:
+                            type: string
+                          values:
+                            items:
+                              type: string
+                            minItems: 1
+                            type: array
+                        required:
+                        - key
+                        - operator
+                        - values
+                        type: object
+                      type: array
+                    matchLabels:
+                      additionalProperties:
+                        type: string
+                      type: object
+                  type: object
+                type: array
+              password:
+                description: Authentication password for routers enforcing TCP MD5
+                  authenticated sessions
+                type: string
+              peerASN:
+                description: AS number to expect from the remote end of the session.
+                format: int32
+                maximum: 4294967295
+                minimum: 0
+                type: integer
+              peerAddress:
+                description: Address to dial when establishing the session.
+                type: string
+              peerPort:
+                description: Port to dial when establishing the session.
+                maximum: 16384
+                minimum: 0
+                type: integer
+              routerID:
+                description: BGP router ID to advertise to the peer
+                type: string
+              sourceAddress:
+                description: Source address to use when establishing the session.
+                type: string
+            required:
+            - myASN
+            - peerASN
+            - peerAddress
+            type: object
+          status:
+            description: BGPPeerStatus defines the observed state of Peer.
+            type: object
+        type: object
+    served: true
+    storage: false
+    subresources:
+      status: {}
+  - name: v1beta2
+    schema:
+      openAPIV3Schema:
+        description: BGPPeer is the Schema for the peers API.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: BGPPeerSpec defines the desired state of Peer.
+            properties:
+              bfdProfile:
+                description: The name of the BFD Profile to be used for the BFD session
+                  associated to the BGP session. If not set, the BFD session won't
+                  be set up.
+                type: string
+              ebgpMultiHop:
+                description: To set if the BGPPeer is multi-hops away. Needed for
+                  FRR mode only.
+                type: boolean
+              holdTime:
+                description: Requested BGP hold time, per RFC4271.
+                type: string
+              keepaliveTime:
+                description: Requested BGP keepalive time, per RFC4271.
+                type: string
+              myASN:
+                description: AS number to use for the local end of the session.
+                format: int32
+                maximum: 4294967295
+                minimum: 0
+                type: integer
+              nodeSelectors:
+                description: Only connect to this peer on nodes that match one of
+                  these selectors.
+                items:
+                  description: A label selector is a label query over a set of resources.
+                    The result of matchLabels and matchExpressions are ANDed. An empty
+                    label selector matches all objects. A null label selector matches
+                    no objects.
+                  properties:
+                    matchExpressions:
+                      description: matchExpressions is a list of label selector requirements.
+                        The requirements are ANDed.
+                      items:
+                        description: A label selector requirement is a selector that
+                          contains values, a key, and an operator that relates the
+                          key and values.
+                        properties:
+                          key:
+                            description: key is the label key that the selector applies
+                              to.
+                            type: string
+                          operator:
+                            description: operator represents a key's relationship
+                              to a set of values. Valid operators are In, NotIn, Exists
+                              and DoesNotExist.
+                            type: string
+                          values:
+                            description: values is an array of string values. If the
+                              operator is In or NotIn, the values array must be non-empty.
+                              If the operator is Exists or DoesNotExist, the values
+                              array must be empty. This array is replaced during a
+                              strategic merge patch.
+                            items:
+                              type: string
+                            type: array
+                        required:
+                        - key
+                        - operator
+                        type: object
+                      type: array
+                    matchLabels:
+                      additionalProperties:
+                        type: string
+                      description: matchLabels is a map of {key,value} pairs. A single
+                        {key,value} in the matchLabels map is equivalent to an element
+                        of matchExpressions, whose key field is "key", the operator
+                        is "In", and the values array contains only "value". The requirements
+                        are ANDed.
+                      type: object
+                  type: object
+                type: array
+              password:
+                description: Authentication password for routers enforcing TCP MD5
+                  authenticated sessions
+                type: string
+              passwordSecret:
+                description: passwordSecret is name of the authentication secret for
+                  BGP Peer. the secret must be of type "kubernetes.io/basic-auth",
+                  and created in the same namespace as the MetalLB deployment. The
+                  password is stored in the secret as the key "password".
+                properties:
+                  name:
+                    description: Name is unique within a namespace to reference a
+                      secret resource.
+                    type: string
+                  namespace:
+                    description: Namespace defines the space within which the secret
+                      name must be unique.
+                    type: string
+                type: object
+              peerASN:
+                description: AS number to expect from the remote end of the session.
+                format: int32
+                maximum: 4294967295
+                minimum: 0
+                type: integer
+              peerAddress:
+                description: Address to dial when establishing the session.
+                type: string
+              peerPort:
+                default: 179
+                description: Port to dial when establishing the session.
+                maximum: 16384
+                minimum: 0
+                type: integer
+              routerID:
+                description: BGP router ID to advertise to the peer
+                type: string
+              sourceAddress:
+                description: Source address to use when establishing the session.
+                type: string
+              vrf:
+                description: To set if we want to peer with the BGPPeer using an interface
+                  belonging to a host vrf
+                type: string
+            required:
+            - myASN
+            - peerASN
+            - peerAddress
+            type: object
+          status:
+            description: BGPPeerStatus defines the observed state of Peer.
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  name: ipaddresspools.metallb.io
+spec:
+  group: metallb.io
+  names:
+    kind: IPAddressPool
+    listKind: IPAddressPoolList
+    plural: ipaddresspools
+    singular: ipaddresspool
+  scope: Namespaced
+  versions:
+  - name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: IPAddressPool represents a pool of IP addresses that can be allocated
+          to LoadBalancer services.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: IPAddressPoolSpec defines the desired state of IPAddressPool.
+            properties:
+              addresses:
+                description: A list of IP address ranges over which MetalLB has authority.
+                  You can list multiple ranges in a single pool, they will all share
+                  the same settings. Each range can be either a CIDR prefix, or an
+                  explicit start-end range of IPs.
+                items:
+                  type: string
+                type: array
+              autoAssign:
+                default: true
+                description: AutoAssign flag used to prevent MetallB from automatic
+                  allocation for a pool.
+                type: boolean
+              avoidBuggyIPs:
+                default: false
+                description: AvoidBuggyIPs prevents addresses ending with .0 and .255
+                  to be used by a pool.
+                type: boolean
+              serviceAllocation:
+                description: AllocateTo makes ip pool allocation to specific namespace
+                  and/or service. The controller will use the pool with lowest value
+                  of priority in case of multiple matches. A pool with no priority
+                  set will be used only if the pools with priority can't be used.
+                  If multiple matching IPAddressPools are available it will check
+                  for the availability of IPs sorting the matching IPAddressPools
+                  by priority, starting from the highest to the lowest. If multiple
+                  IPAddressPools have the same priority, choice will be random.
+                properties:
+                  namespaceSelectors:
+                    description: NamespaceSelectors list of label selectors to select
+                      namespace(s) for ip pool, an alternative to using namespace
+                      list.
+                    items:
+                      description: A label selector is a label query over a set of
+                        resources. The result of matchLabels and matchExpressions
+                        are ANDed. An empty label selector matches all objects. A
+                        null label selector matches no objects.
+                      properties:
+                        matchExpressions:
+                          description: matchExpressions is a list of label selector
+                            requirements. The requirements are ANDed.
+                          items:
+                            description: A label selector requirement is a selector
+                              that contains values, a key, and an operator that relates
+                              the key and values.
+                            properties:
+                              key:
+                                description: key is the label key that the selector
+                                  applies to.
+                                type: string
+                              operator:
+                                description: operator represents a key's relationship
+                                  to a set of values. Valid operators are In, NotIn,
+                                  Exists and DoesNotExist.
+                                type: string
+                              values:
+                                description: values is an array of string values.
+                                  If the operator is In or NotIn, the values array
+                                  must be non-empty. If the operator is Exists or
+                                  DoesNotExist, the values array must be empty. This
+                                  array is replaced during a strategic merge patch.
+                                items:
+                                  type: string
+                                type: array
+                            required:
+                            - key
+                            - operator
+                            type: object
+                          type: array
+                        matchLabels:
+                          additionalProperties:
+                            type: string
+                          description: matchLabels is a map of {key,value} pairs.
+                            A single {key,value} in the matchLabels map is equivalent
+                            to an element of matchExpressions, whose key field is
+                            "key", the operator is "In", and the values array contains
+                            only "value". The requirements are ANDed.
+                          type: object
+                      type: object
+                    type: array
+                  namespaces:
+                    description: Namespaces list of namespace(s) on which ip pool
+                      can be attached.
+                    items:
+                      type: string
+                    type: array
+                  priority:
+                    description: Priority priority given for ip pool while ip allocation
+                      on a service.
+                    type: integer
+                  serviceSelectors:
+                    description: ServiceSelectors list of label selector to select
+                      service(s) for which ip pool can be used for ip allocation.
+                    items:
+                      description: A label selector is a label query over a set of
+                        resources. The result of matchLabels and matchExpressions
+                        are ANDed. An empty label selector matches all objects. A
+                        null label selector matches no objects.
+                      properties:
+                        matchExpressions:
+                          description: matchExpressions is a list of label selector
+                            requirements. The requirements are ANDed.
+                          items:
+                            description: A label selector requirement is a selector
+                              that contains values, a key, and an operator that relates
+                              the key and values.
+                            properties:
+                              key:
+                                description: key is the label key that the selector
+                                  applies to.
+                                type: string
+                              operator:
+                                description: operator represents a key's relationship
+                                  to a set of values. Valid operators are In, NotIn,
+                                  Exists and DoesNotExist.
+                                type: string
+                              values:
+                                description: values is an array of string values.
+                                  If the operator is In or NotIn, the values array
+                                  must be non-empty. If the operator is Exists or
+                                  DoesNotExist, the values array must be empty. This
+                                  array is replaced during a strategic merge patch.
+                                items:
+                                  type: string
+                                type: array
+                            required:
+                            - key
+                            - operator
+                            type: object
+                          type: array
+                        matchLabels:
+                          additionalProperties:
+                            type: string
+                          description: matchLabels is a map of {key,value} pairs.
+                            A single {key,value} in the matchLabels map is equivalent
+                            to an element of matchExpressions, whose key field is
+                            "key", the operator is "In", and the values array contains
+                            only "value". The requirements are ANDed.
+                          type: object
+                      type: object
+                    type: array
+                type: object
+            required:
+            - addresses
+            type: object
+          status:
+            description: IPAddressPoolStatus defines the observed state of IPAddressPool.
+            type: object
+        required:
+        - spec
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  name: l2advertisements.metallb.io
+spec:
+  group: metallb.io
+  names:
+    kind: L2Advertisement
+    listKind: L2AdvertisementList
+    plural: l2advertisements
+    singular: l2advertisement
+  scope: Namespaced
+  versions:
+  - name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: L2Advertisement allows to advertise the LoadBalancer IPs provided
+          by the selected pools via L2.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: L2AdvertisementSpec defines the desired state of L2Advertisement.
+            properties:
+              interfaces:
+                description: A list of interfaces to announce from. The LB IP will
+                  be announced only from these interfaces. If the field is not set,
+                  we advertise from all the interfaces on the host.
+                items:
+                  type: string
+                type: array
+              ipAddressPoolSelectors:
+                description: A selector for the IPAddressPools which would get advertised
+                  via this advertisement. If no IPAddressPool is selected by this
+                  or by the list, the advertisement is applied to all the IPAddressPools.
+                items:
+                  description: A label selector is a label query over a set of resources.
+                    The result of matchLabels and matchExpressions are ANDed. An empty
+                    label selector matches all objects. A null label selector matches
+                    no objects.
+                  properties:
+                    matchExpressions:
+                      description: matchExpressions is a list of label selector requirements.
+                        The requirements are ANDed.
+                      items:
+                        description: A label selector requirement is a selector that
+                          contains values, a key, and an operator that relates the
+                          key and values.
+                        properties:
+                          key:
+                            description: key is the label key that the selector applies
+                              to.
+                            type: string
+                          operator:
+                            description: operator represents a key's relationship
+                              to a set of values. Valid operators are In, NotIn, Exists
+                              and DoesNotExist.
+                            type: string
+                          values:
+                            description: values is an array of string values. If the
+                              operator is In or NotIn, the values array must be non-empty.
+                              If the operator is Exists or DoesNotExist, the values
+                              array must be empty. This array is replaced during a
+                              strategic merge patch.
+                            items:
+                              type: string
+                            type: array
+                        required:
+                        - key
+                        - operator
+                        type: object
+                      type: array
+                    matchLabels:
+                      additionalProperties:
+                        type: string
+                      description: matchLabels is a map of {key,value} pairs. A single
+                        {key,value} in the matchLabels map is equivalent to an element
+                        of matchExpressions, whose key field is "key", the operator
+                        is "In", and the values array contains only "value". The requirements
+                        are ANDed.
+                      type: object
+                  type: object
+                type: array
+              ipAddressPools:
+                description: The list of IPAddressPools to advertise via this advertisement,
+                  selected by name.
+                items:
+                  type: string
+                type: array
+              nodeSelectors:
+                description: NodeSelectors allows to limit the nodes to announce as
+                  next hops for the LoadBalancer IP. When empty, all the nodes having  are
+                  announced as next hops.
+                items:
+                  description: A label selector is a label query over a set of resources.
+                    The result of matchLabels and matchExpressions are ANDed. An empty
+                    label selector matches all objects. A null label selector matches
+                    no objects.
+                  properties:
+                    matchExpressions:
+                      description: matchExpressions is a list of label selector requirements.
+                        The requirements are ANDed.
+                      items:
+                        description: A label selector requirement is a selector that
+                          contains values, a key, and an operator that relates the
+                          key and values.
+                        properties:
+                          key:
+                            description: key is the label key that the selector applies
+                              to.
+                            type: string
+                          operator:
+                            description: operator represents a key's relationship
+                              to a set of values. Valid operators are In, NotIn, Exists
+                              and DoesNotExist.
+                            type: string
+                          values:
+                            description: values is an array of string values. If the
+                              operator is In or NotIn, the values array must be non-empty.
+                              If the operator is Exists or DoesNotExist, the values
+                              array must be empty. This array is replaced during a
+                              strategic merge patch.
+                            items:
+                              type: string
+                            type: array
+                        required:
+                        - key
+                        - operator
+                        type: object
+                      type: array
+                    matchLabels:
+                      additionalProperties:
+                        type: string
+                      description: matchLabels is a map of {key,value} pairs. A single
+                        {key,value} in the matchLabels map is equivalent to an element
+                        of matchExpressions, whose key field is "key", the operator
+                        is "In", and the values array contains only "value". The requirements
+                        are ANDed.
+                      type: object
+                  type: object
+                type: array
+            type: object
+          status:
+            description: L2AdvertisementStatus defines the observed state of L2Advertisement.
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.7.0
+  creationTimestamp: null
+  name: communities.metallb.io
+spec:
+  group: metallb.io
+  names:
+    kind: Community
+    listKind: CommunityList
+    plural: communities
+    singular: community
+  scope: Namespaced
+  versions:
+  - name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: Community is a collection of aliases for communities. Users can
+          define named aliases to be used in the BGPPeer CRD.
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: CommunitySpec defines the desired state of Community.
+            properties:
+              communities:
+                items:
+                  properties:
+                    name:
+                      description: The name of the alias for the community.
+                      type: string
+                    value:
+                      description: The BGP community value corresponding to the given
+                        name.
+                      type: string
+                  type: object
+                type: array
+            type: object
+          status:
+            description: CommunityStatus defines the observed state of Community.
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
diff --git a/charts/metallb/policy/controller.rego b/charts/metallb/policy/controller.rego
new file mode 100644
index 0000000..716eeb7
--- /dev/null
+++ b/charts/metallb/policy/controller.rego
@@ -0,0 +1,16 @@
+package main
+
+# validate serviceAccountName
+deny[msg] {
+  input.kind == "Deployment"
+  serviceAccountName := input.spec.template.spec.serviceAccountName
+  not serviceAccountName == "RELEASE-NAME-metallb-controller"
+  msg = sprintf("controller serviceAccountName '%s' does not match expected value", [serviceAccountName])
+}
+
+# validate node selector includes builtin when custom ones are provided
+deny[msg] {
+  input.kind == "Deployment"
+  not input.spec.template.spec.nodeSelector["kubernetes.io/os"] == "linux"
+  msg = "controller nodeSelector does not include '\"kubernetes.io/os\": linux'"
+}
diff --git a/charts/metallb/policy/rbac.rego b/charts/metallb/policy/rbac.rego
new file mode 100644
index 0000000..047345e
--- /dev/null
+++ b/charts/metallb/policy/rbac.rego
@@ -0,0 +1,27 @@
+package main
+
+# Validate PSP exists in ClusterRole :controller
+deny[msg] {
+  input.kind == "ClusterRole"
+  input.metadata.name == "metallb:controller"
+  input.rules[3] == {
+	"apiGroups": ["policy"],
+	"resources": ["podsecuritypolicies"],
+	"resourceNames": ["metallb-controller"],
+	"verbs": ["use"]
+  }
+  msg = "ClusterRole metallb:controller does not include PSP rule"
+}
+
+# Validate PSP exists in ClusterRole :speaker
+deny[msg] {
+  input.kind == "ClusterRole"
+  input.metadata.name == "metallb:speaker"
+  input.rules[3] == {
+	"apiGroups": ["policy"],
+	"resources": ["podsecuritypolicies"],
+	"resourceNames": ["metallb-controller"],
+	"verbs": ["use"]
+  }
+  msg = "ClusterRole metallb:speaker does not include PSP rule"
+}
diff --git a/charts/metallb/policy/speaker.rego b/charts/metallb/policy/speaker.rego
new file mode 100644
index 0000000..d4d8137
--- /dev/null
+++ b/charts/metallb/policy/speaker.rego
@@ -0,0 +1,30 @@
+package main
+
+# validate serviceAccountName
+deny[msg] {
+  input.kind == "DaemonSet"
+  serviceAccountName := input.spec.template.spec.serviceAccountName
+  not serviceAccountName == "RELEASE-NAME-metallb-speaker"
+  msg = sprintf("speaker serviceAccountName '%s' does not match expected value", [serviceAccountName])
+}
+
+# validate METALLB_ML_SECRET_KEY (memberlist)
+deny[msg] {
+	input.kind == "DaemonSet"
+	not input.spec.template.spec.containers[0].env[5].name == "METALLB_ML_SECRET_KEY_PATH"
+	msg = "speaker env does not contain METALLB_ML_SECRET_KEY_PATH at env[5]"
+}
+
+# validate node selector includes builtin when custom ones are provided
+deny[msg] {
+  input.kind == "DaemonSet"
+  not input.spec.template.spec.nodeSelector["kubernetes.io/os"] == "linux"
+  msg = "controller nodeSelector does not include '\"kubernetes.io/os\": linux'"
+}
+
+# validate tolerations include the builtins when custom ones are provided
+deny[msg] {
+  input.kind == "DaemonSet"
+  not input.spec.template.spec.tolerations[0] == { "key": "node-role.kubernetes.io/master", "effect": "NoSchedule", "operator": "Exists" }
+  msg = "controller tolerations does not include node-role.kubernetes.io/master:NoSchedule"
+}
diff --git a/charts/metallb/templates/NOTES.txt b/charts/metallb/templates/NOTES.txt
new file mode 100644
index 0000000..23d1d5b
--- /dev/null
+++ b/charts/metallb/templates/NOTES.txt
@@ -0,0 +1,4 @@
+MetalLB is now running in the cluster.
+
+Now you can configure it via its CRs. Please refer to the metallb official docs
+on how to use the CRs.
diff --git a/charts/metallb/templates/_helpers.tpl b/charts/metallb/templates/_helpers.tpl
new file mode 100644
index 0000000..53d9528
--- /dev/null
+++ b/charts/metallb/templates/_helpers.tpl
@@ -0,0 +1,113 @@
+{{/* vim: set filetype=mustache: */}}
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "metallb.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
+{{- 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 "metallb.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 }}
+
+{{/*
+Create chart name and version as used by the chart label.
+*/}}
+{{- define "metallb.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Common labels
+*/}}
+{{- define "metallb.labels" -}}
+helm.sh/chart: {{ include "metallb.chart" . }}
+{{ include "metallb.selectorLabels" . }}
+{{- if .Chart.AppVersion }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+{{- end }}
+
+{{/*
+Selector labels
+*/}}
+{{- define "metallb.selectorLabels" -}}
+app.kubernetes.io/name: {{ include "metallb.name" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end }}
+
+{{/*
+Create the name of the controller service account to use
+*/}}
+{{- define "metallb.controller.serviceAccountName" -}}
+{{- if .Values.controller.serviceAccount.create }}
+{{- default (printf "%s-controller" (include "metallb.fullname" .)) .Values.controller.serviceAccount.name }}
+{{- else }}
+{{- default "default" .Values.controller.serviceAccount.name }}
+{{- end }}
+{{- end }}
+
+{{/*
+Create the name of the speaker service account to use
+*/}}
+{{- define "metallb.speaker.serviceAccountName" -}}
+{{- if .Values.speaker.serviceAccount.create }}
+{{- default (printf "%s-speaker" (include "metallb.fullname" .)) .Values.speaker.serviceAccount.name }}
+{{- else }}
+{{- default "default" .Values.speaker.serviceAccount.name }}
+{{- end }}
+{{- end }}
+
+{{/*
+Create the name of the settings Secret to use.
+*/}}
+{{- define "metallb.secretName" -}}
+    {{ default ( printf "%s-memberlist" (include "metallb.fullname" .)) .Values.speaker.secretName | trunc 63 | trimSuffix "-" }}
+{{- end -}}
+
+{{- define "metrics.exposedportname" -}}
+{{- if .Values.prometheus.secureMetricsPort -}}
+"metricshttps"
+{{- else -}}
+"metrics"
+{{- end -}}
+{{- end -}}
+
+{{- define "metrics.exposedfrrportname" -}}
+{{- if .Values.speaker.frr.secureMetricsPort -}}
+"frrmetricshttps"
+{{- else -}}
+"frrmetrics"
+{{- end }}
+{{- end }}
+
+{{- define "metrics.exposedport" -}}
+{{- if .Values.prometheus.secureMetricsPort -}}
+{{ .Values.prometheus.secureMetricsPort }}
+{{- else -}}
+{{ .Values.prometheus.metricsPort }}
+{{- end -}}
+{{- end }}
+
+{{- define "metrics.exposedfrrport" -}}
+{{- if .Values.speaker.frr.secureMetricsPort -}}
+{{ .Values.speaker.frr.secureMetricsPort }}
+{{- else -}}
+{{ .Values.speaker.frr.metricsPort }}
+{{- end }}
+{{- end }}
diff --git a/charts/metallb/templates/controller.yaml b/charts/metallb/templates/controller.yaml
new file mode 100644
index 0000000..2b522d1
--- /dev/null
+++ b/charts/metallb/templates/controller.yaml
@@ -0,0 +1,182 @@
+{{- if .Values.controller.enabled }}
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{ template "metallb.fullname" . }}-controller
+  namespace: {{ .Release.Namespace | quote }}
+  labels:
+    {{- include "metallb.labels" . | nindent 4 }}
+    app.kubernetes.io/component: controller
+    {{- range $key, $value := .Values.controller.labels }}
+    {{ $key }}: {{ $value | quote }}
+    {{- end }}
+spec:
+  {{- if .Values.controller.strategy }}
+  strategy: {{- toYaml .Values.controller.strategy | nindent 4 }}
+  {{- end }}
+  selector:
+    matchLabels:
+      {{- include "metallb.selectorLabels" . | nindent 6 }}
+      app.kubernetes.io/component: controller
+  template:
+    metadata:
+      {{- if or .Values.prometheus.scrapeAnnotations .Values.controller.podAnnotations }}
+      annotations:
+        {{- if .Values.prometheus.scrapeAnnotations }}
+        prometheus.io/scrape: "true"
+        prometheus.io/port: "{{ .Values.prometheus.metricsPort }}"
+        {{- end }}
+        {{- with .Values.controller.podAnnotations }}
+          {{- toYaml . | nindent 8 }}
+        {{- end }}
+      {{- end }}
+      labels:
+        {{- include "metallb.selectorLabels" . | nindent 8 }}
+        app.kubernetes.io/component: controller
+        {{- range $key, $value := .Values.controller.labels }}
+        {{ $key }}: {{ $value | quote }}
+        {{- end }}
+    spec:
+      {{- with .Values.controller.runtimeClassName }}
+      runtimeClassName: {{ . | quote }}
+      {{- end }}
+      {{- with .Values.imagePullSecrets }}
+      imagePullSecrets:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      serviceAccountName: {{ template "metallb.controller.serviceAccountName" . }}
+      terminationGracePeriodSeconds: 0
+{{- if .Values.controller.securityContext }}
+      securityContext:
+{{ toYaml .Values.controller.securityContext | indent 8 }}
+{{- end }}
+      containers:
+      - name: controller
+        image: {{ .Values.controller.image.repository }}:{{ .Values.controller.image.tag | default .Chart.AppVersion }}
+        {{- if .Values.controller.image.pullPolicy }}
+        imagePullPolicy: {{ .Values.controller.image.pullPolicy }}
+        {{- end }}
+        {{- if .Values.controller.command }}
+        command:
+          - {{ .Values.controller.command }}
+        {{- end }}
+        args:
+        - --port={{ .Values.prometheus.metricsPort }}
+        {{- with .Values.controller.logLevel }}
+        - --log-level={{ . }}
+        {{- end }}
+        - --cert-service-name=metallb-webhook-service
+        {{- if .Values.loadBalancerClass }}
+        - --lb-class={{ .Values.loadBalancerClass }}
+        {{- end }}
+        {{- if .Values.controller.webhookMode }}
+        - --webhook-mode={{ .Values.controller.webhookMode }}
+        {{- end }}
+        env:
+        {{- if and .Values.speaker.enabled .Values.speaker.memberlist.enabled }}
+        - name: METALLB_ML_SECRET_NAME
+          value: {{ include "metallb.secretName" . }}
+        - name: METALLB_DEPLOYMENT
+          value: {{ template "metallb.fullname" . }}-controller
+        {{- end }}
+        {{- if .Values.speaker.frr.enabled }}
+        - name: METALLB_BGP_TYPE
+          value: frr
+        {{- end }}
+        ports:
+        - name: monitoring
+          containerPort: {{ .Values.prometheus.metricsPort }}
+        - containerPort: 9443
+          name: webhook-server
+          protocol: TCP
+        volumeMounts:
+        - mountPath: /tmp/k8s-webhook-server/serving-certs
+          name: cert
+          readOnly: true
+        {{- if .Values.controller.livenessProbe.enabled }}
+        livenessProbe:
+          httpGet:
+            path: /metrics
+            port: monitoring
+          initialDelaySeconds: {{ .Values.controller.livenessProbe.initialDelaySeconds }}
+          periodSeconds: {{ .Values.controller.livenessProbe.periodSeconds }}
+          timeoutSeconds: {{ .Values.controller.livenessProbe.timeoutSeconds }}
+          successThreshold: {{ .Values.controller.livenessProbe.successThreshold }}
+          failureThreshold: {{ .Values.controller.livenessProbe.failureThreshold }}
+        {{- end }}
+        {{- if .Values.controller.readinessProbe.enabled }}
+        readinessProbe:
+          httpGet:
+            path: /metrics
+            port: monitoring
+          initialDelaySeconds: {{ .Values.controller.readinessProbe.initialDelaySeconds }}
+          periodSeconds: {{ .Values.controller.readinessProbe.periodSeconds }}
+          timeoutSeconds: {{ .Values.controller.readinessProbe.timeoutSeconds }}
+          successThreshold: {{ .Values.controller.readinessProbe.successThreshold }}
+          failureThreshold: {{ .Values.controller.readinessProbe.failureThreshold }}
+        {{- end }}
+        {{- with .Values.controller.resources }}
+        resources:
+          {{- toYaml . | nindent 10 }}
+        {{- end }}
+        securityContext:
+          allowPrivilegeEscalation: false
+          readOnlyRootFilesystem: true
+          capabilities:
+            drop:
+            - ALL
+      {{- if .Values.prometheus.secureMetricsPort }}
+      - name: kube-rbac-proxy
+        image: {{ .Values.prometheus.rbacProxy.repository }}:{{ .Values.prometheus.rbacProxy.tag }}
+        imagePullPolicy: {{ .Values.prometheus.rbacProxy.pullPolicy }}
+        args:
+          - --logtostderr
+          - --secure-listen-address=:{{ .Values.prometheus.secureMetricsPort }}
+          - --upstream=http://127.0.0.1:{{ .Values.prometheus.metricsPort }}/
+          - --tls-cipher-suites=TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_RSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256
+        {{- if .Values.prometheus.controllerMetricsTLSSecret }}
+          - --tls-private-key-file=/etc/metrics/tls.key
+          - --tls-cert-file=/etc/metrics/tls.crt
+        {{- end }}
+        ports:
+          - containerPort: {{ .Values.prometheus.secureMetricsPort }}
+            name: metricshttps
+        resources:
+          requests:
+            cpu: 10m
+            memory: 20Mi
+        terminationMessagePolicy: FallbackToLogsOnError
+        {{- if .Values.prometheus.controllerMetricsTLSSecret }}
+        volumeMounts:
+          - name: metrics-certs
+            mountPath: /etc/metrics
+            readOnly: true
+        {{- end }}
+      {{ end }}
+      nodeSelector:
+        "kubernetes.io/os": linux
+        {{- with .Values.controller.nodeSelector }}
+          {{- toYaml . | nindent 8 }}
+        {{- end }}
+      {{- with .Values.controller.affinity }}
+      affinity:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      {{- with .Values.controller.tolerations }}
+      tolerations:
+        {{- toYaml . | nindent 6 }}
+      {{- end }}
+      {{- with .Values.controller.priorityClassName }}
+      priorityClassName: {{ . | quote }}
+      {{- end }}
+      volumes:
+      - name: cert
+        secret:
+          defaultMode: 420
+          secretName: webhook-server-cert
+      {{- if .Values.prometheus.controllerMetricsTLSSecret }}
+      - name: metrics-certs
+        secret:
+          secretName: {{ .Values.prometheus.controllerMetricsTLSSecret }}
+      {{- end }}
+{{- end }}
diff --git a/charts/metallb/templates/deprecated_configInline.yaml b/charts/metallb/templates/deprecated_configInline.yaml
new file mode 100644
index 0000000..8a1a551
--- /dev/null
+++ b/charts/metallb/templates/deprecated_configInline.yaml
@@ -0,0 +1,3 @@
+{{- if .Values.configInline }}
+{{- fail "Starting from v0.13.0 configInline is no longer supported. Please see https://metallb.universe.tf/#backward-compatibility" }}
+{{- end }}
diff --git a/charts/metallb/templates/exclude-l2-config.yaml b/charts/metallb/templates/exclude-l2-config.yaml
new file mode 100644
index 0000000..cacea8f
--- /dev/null
+++ b/charts/metallb/templates/exclude-l2-config.yaml
@@ -0,0 +1,23 @@
+{{- if .Values.speaker.excludeInterfaces.enabled }}
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: metallb-excludel2
+  namespace: {{ .Release.Namespace | quote }}
+data:
+  excludel2.yaml: |
+    announcedInterfacesToExclude:
+    - ^docker.*
+    - ^cbr.*
+    - ^dummy.*
+    - ^virbr.*
+    - ^lxcbr.*
+    - ^veth.*
+    - ^lo$
+    - ^cali.*
+    - ^tunl.*
+    - ^flannel.*
+    - ^kube-ipvs.*
+    - ^cni.*
+    - ^nodelocaldns.*
+{{- end }}
\ No newline at end of file
diff --git a/charts/metallb/templates/podmonitor.yaml b/charts/metallb/templates/podmonitor.yaml
new file mode 100644
index 0000000..93a7fd6
--- /dev/null
+++ b/charts/metallb/templates/podmonitor.yaml
@@ -0,0 +1,106 @@
+{{- if .Values.prometheus.podMonitor.enabled }}
+apiVersion: monitoring.coreos.com/v1
+kind: PodMonitor
+metadata:
+  name: {{ template "metallb.fullname" . }}-controller
+  labels:
+    {{- include "metallb.labels" . | nindent 4 }}
+    app.kubernetes.io/component: controller
+    {{- if .Values.prometheus.podMonitor.additionalLabels }}
+{{ toYaml .Values.prometheus.podMonitor.additionalLabels | indent 4 }}
+    {{- end }}
+  {{- if .Values.prometheus.podMonitor.annotations }}
+  annotations:
+{{ toYaml .Values.prometheus.podMonitor.annotations | indent 4 }}
+  {{- end }}
+spec:
+  jobLabel: {{ .Values.prometheus.podMonitor.jobLabel | quote }}
+  selector:
+    matchLabels:
+      {{- include "metallb.selectorLabels" . | nindent 6 }}
+      app.kubernetes.io/component: controller
+  namespaceSelector:
+    matchNames:
+    - {{ .Release.Namespace }}
+  podMetricsEndpoints:
+  - port: monitoring
+    path: /metrics
+    {{- if .Values.prometheus.podMonitor.interval }}
+    interval: {{ .Values.prometheus.podMonitor.interval }}
+    {{- end }}
+{{- if .Values.prometheus.podMonitor.metricRelabelings }}
+    metricRelabelings:
+{{- toYaml .Values.prometheus.podMonitor.metricRelabelings | nindent 4 }}
+{{- end }}
+{{- if .Values.prometheus.podMonitor.relabelings }}
+    relabelings:
+{{- toYaml .Values.prometheus.podMonitor.relabelings | nindent 4 }}
+{{- end }}
+---
+apiVersion: monitoring.coreos.com/v1
+kind: PodMonitor
+metadata:
+  name: {{ template "metallb.fullname" . }}-speaker
+  labels:
+    {{- include "metallb.labels" . | nindent 4 }}
+    app.kubernetes.io/component: speaker
+    {{- if .Values.prometheus.podMonitor.additionalLabels }}
+{{ toYaml .Values.prometheus.podMonitor.additionalLabels | indent 4 }}
+    {{- end }}
+  {{- if .Values.prometheus.podMonitor.annotations }}
+  annotations:
+{{ toYaml .Values.prometheus.podMonitor.annotations | indent 4 }}
+  {{- end }}
+spec:
+  jobLabel: {{ .Values.prometheus.podMonitor.jobLabel | quote }}
+  selector:
+    matchLabels:
+      {{- include "metallb.selectorLabels" . | nindent 6 }}
+      app.kubernetes.io/component: speaker
+  namespaceSelector:
+    matchNames:
+    - {{ .Release.Namespace }}
+  podMetricsEndpoints:
+  - port: monitoring
+    path: /metrics
+    {{- if .Values.prometheus.podMonitor.interval }}
+    interval: {{ .Values.prometheus.podMonitor.interval }}
+    {{- end }}
+{{- if .Values.prometheus.podMonitor.metricRelabelings }}
+    metricRelabelings:
+{{- toYaml .Values.prometheus.podMonitor.metricRelabelings | nindent 4 }}
+{{- end }}
+{{- if .Values.prometheus.podMonitor.relabelings }}
+    relabelings:
+{{- toYaml .Values.prometheus.podMonitor.relabelings | nindent 4 }}
+{{- end }}
+---
+{{- if .Values.prometheus.rbacPrometheus }}
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: {{ template "metallb.fullname" . }}-prometheus
+rules:
+  - apiGroups:
+      - ""
+    resources:
+      - pods
+    verbs:
+      - get
+      - list
+      - watch
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  name: {{ template "metallb.fullname" . }}-prometheus
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: {{ template "metallb.fullname" . }}-prometheus
+subjects:
+  - kind: ServiceAccount
+    name: {{ required ".Values.prometheus.serviceAccount must be defined when .Values.prometheus.podMonitor.enabled == true" .Values.prometheus.serviceAccount }}
+    namespace: {{ required ".Values.prometheus.namespace must be defined when .Values.prometheus.podMonitor.enabled == true" .Values.prometheus.namespace }}
+{{- end }}
+{{- end }}
diff --git a/charts/metallb/templates/prometheusrules.yaml b/charts/metallb/templates/prometheusrules.yaml
new file mode 100644
index 0000000..463aaca
--- /dev/null
+++ b/charts/metallb/templates/prometheusrules.yaml
@@ -0,0 +1,84 @@
+{{- if .Values.prometheus.prometheusRule.enabled }}
+apiVersion: monitoring.coreos.com/v1
+kind: PrometheusRule
+metadata:
+  name: {{ template "metallb.fullname" . }}
+  labels:
+    {{- include "metallb.labels" . | nindent 4 }}
+    {{- if .Values.prometheus.prometheusRule.additionalLabels }}
+{{ toYaml .Values.prometheus.prometheusRule.additionalLabels | indent 4 }}
+    {{- end }}
+  {{- if .Values.prometheus.prometheusRule.annotations }}
+  annotations:
+{{ toYaml .Values.prometheus.prometheusRule.annotations | indent 4 }}
+  {{- end }}
+spec:
+  groups:
+  - name: {{ template "metallb.fullname" . }}.rules
+    rules:
+    {{- if .Values.prometheus.prometheusRule.staleConfig.enabled }}
+    - alert: MetalLBStaleConfig
+      annotations:
+        message: {{`'{{ $labels.job }} - MetalLB {{ $labels.container }} on {{ $labels.pod
+          }} has a stale config for > 1 minute'`}}
+      expr: metallb_k8s_client_config_stale_bool{job="{{ include "metallb.name" . }}"} == 1
+      for: 1m
+      {{- with .Values.prometheus.prometheusRule.staleConfig.labels }}
+      labels:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+    {{- end }}
+    {{- if .Values.prometheus.prometheusRule.configNotLoaded.enabled }}
+    - alert: MetalLBConfigNotLoaded
+      annotations:
+        message: {{`'{{ $labels.job }} - MetalLB {{ $labels.container }} on {{ $labels.pod
+          }} has not loaded for > 1 minute'`}}
+      expr: metallb_k8s_client_config_loaded_bool{job="{{ include "metallb.name" . }}"} == 0
+      for: 1m
+      {{- with .Values.prometheus.prometheusRule.configNotLoaded.labels }}
+      labels:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+    {{- end }}
+    {{- if .Values.prometheus.prometheusRule.addressPoolExhausted.enabled }}
+    - alert: MetalLBAddressPoolExhausted
+      annotations:
+        message: {{`'{{ $labels.job }} - MetalLB {{ $labels.container }} on {{ $labels.pod
+          }} has exhausted address pool {{ $labels.pool }} for > 1 minute'`}}
+      expr: metallb_allocator_addresses_in_use_total >= on(pool) metallb_allocator_addresses_total
+      for: 1m
+      {{- with .Values.prometheus.prometheusRule.addressPoolExhausted.labels }}
+      labels:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+    {{- end }}
+
+    {{- if .Values.prometheus.prometheusRule.addressPoolUsage.enabled }}
+    {{- range .Values.prometheus.prometheusRule.addressPoolUsage.thresholds }}
+    - alert: MetalLBAddressPoolUsage{{ .percent }}Percent
+      annotations:
+        message: {{`'{{ $labels.job }} - MetalLB {{ $labels.container }} on {{ $labels.pod
+          }} has address pool {{ $labels.pool }} past `}}{{ .percent }}{{`% usage for > 1 minute'`}}
+      expr: ( metallb_allocator_addresses_in_use_total / on(pool) metallb_allocator_addresses_total ) * 100 > {{ .percent }}
+      {{- with .labels }}
+      labels:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+    {{- end }}
+    {{- end }}
+    {{- if .Values.prometheus.prometheusRule.bgpSessionDown.enabled }}
+    - alert: MetalLBBGPSessionDown
+      annotations:
+        message: {{`'{{ $labels.job }} - MetalLB {{ $labels.container }} on {{ $labels.pod
+          }} has BGP session {{ $labels.peer }} down for > 1 minute'`}}
+      expr: metallb_bgp_session_up{job="{{ include "metallb.name" . }}"} == 0
+      for: 1m
+      {{- with .Values.prometheus.prometheusRule.bgpSessionDown.labels }}
+      labels:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+    {{- end }}
+    {{- with .Values.prometheus.prometheusRule.extraAlerts }}
+    {{- toYaml . | nindent 4 }}
+    {{- end}}
+{{- end }}
diff --git a/charts/metallb/templates/rbac.yaml b/charts/metallb/templates/rbac.yaml
new file mode 100644
index 0000000..ed6b826
--- /dev/null
+++ b/charts/metallb/templates/rbac.yaml
@@ -0,0 +1,210 @@
+{{- if .Values.rbac.create -}}
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: {{ template "metallb.fullname" . }}:controller
+  labels:
+    {{- include "metallb.labels" . | nindent 4 }}
+rules:
+- apiGroups: [""]
+  resources: ["services", "namespaces"]
+  verbs: ["get", "list", "watch"]
+- apiGroups: [""]
+  resources: ["nodes"]
+  verbs: ["list"]
+- apiGroups: [""]
+  resources: ["services/status"]
+  verbs: ["update"]
+- apiGroups: [""]
+  resources: ["events"]
+  verbs: ["create", "patch"]
+- apiGroups: ["admissionregistration.k8s.io"]
+  resources: ["validatingwebhookconfigurations", "mutatingwebhookconfigurations"]
+  resourceNames: ["metallb-webhook-configuration"]
+  verbs: ["create", "delete", "get", "list", "patch", "update", "watch"]
+- apiGroups: ["admissionregistration.k8s.io"]
+  resources: ["validatingwebhookconfigurations", "mutatingwebhookconfigurations"]
+  verbs: ["list", "watch"]
+- apiGroups: ["apiextensions.k8s.io"]
+  resources: ["customresourcedefinitions"]
+  resourceNames: ["addresspools.metallb.io","bfdprofiles.metallb.io","bgpadvertisements.metallb.io",
+    "bgppeers.metallb.io","ipaddresspools.metallb.io","l2advertisements.metallb.io","communities.metallb.io"]
+  verbs: ["create", "delete", "get", "list", "patch", "update", "watch"]
+- apiGroups: ["apiextensions.k8s.io"]
+  resources: ["customresourcedefinitions"]
+  verbs: ["list", "watch"]
+{{- if .Values.prometheus.secureMetricsPort }}
+- apiGroups: ["authentication.k8s.io"]
+  resources: ["tokenreviews"]
+  verbs: ["create"]
+- apiGroups: ["authorization.k8s.io"]
+  resources: ["subjectaccessreviews"]
+  verbs: ["create"]
+{{- end }}
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: {{ template "metallb.fullname" . }}:speaker
+  labels:
+    {{- include "metallb.labels" . | nindent 4 }}
+rules:
+- apiGroups: [""]
+  resources: ["services", "endpoints", "nodes", "namespaces"]
+  verbs: ["get", "list", "watch"]
+- apiGroups: ["discovery.k8s.io"]
+  resources: ["endpointslices"]
+  verbs: ["get", "list", "watch"]
+- apiGroups: [""]
+  resources: ["events"]
+  verbs: ["create", "patch"]
+{{- if .Values.prometheus.secureMetricsPort }}
+- apiGroups: ["authentication.k8s.io"]
+  resources: ["tokenreviews"]
+  verbs: ["create"]
+- apiGroups: ["authorization.k8s.io"]
+  resources: ["subjectaccessreviews"]
+  verbs: ["create"]
+{{- end }}
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: {{ include "metallb.fullname" . }}-pod-lister
+  namespace: {{ .Release.Namespace | quote }}
+  labels: {{- include "metallb.labels" . | nindent 4 }}
+rules:
+- apiGroups: [""]
+  resources: ["pods"]
+  verbs: ["list"]
+- apiGroups: [""]
+  resources: ["secrets"]
+  verbs: ["get", "list", "watch"]
+- apiGroups: [""]
+  resources: ["configmaps"]
+  verbs: ["get", "list", "watch"]
+- apiGroups: ["metallb.io"]
+  resources: ["addresspools"]
+  verbs: ["get", "list", "watch"]
+- apiGroups: ["metallb.io"]
+  resources: ["bfdprofiles"]
+  verbs: ["get", "list", "watch"]
+- apiGroups: ["metallb.io"]
+  resources: ["bgppeers"]
+  verbs: ["get", "list", "watch"]
+- apiGroups: ["metallb.io"]
+  resources: ["l2advertisements"]
+  verbs: ["get", "list", "watch"]
+- apiGroups: ["metallb.io"]
+  resources: ["bgpadvertisements"]
+  verbs: ["get", "list", "watch"]
+- apiGroups: ["metallb.io"]
+  resources: ["ipaddresspools"]
+  verbs: ["get", "list", "watch"]
+- apiGroups: ["metallb.io"]
+  resources: ["communities"]
+  verbs: ["get", "list", "watch"]
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: {{ include "metallb.fullname" . }}-controller
+  namespace: {{ .Release.Namespace | quote }}
+  labels: {{- include "metallb.labels" . | nindent 4 }}
+rules:
+{{- if .Values.speaker.memberlist.enabled }}
+- apiGroups: [""]
+  resources: ["secrets"]
+  verbs: ["create", "get", "list", "watch"]
+- apiGroups: [""]
+  resources: ["secrets"]
+  resourceNames: [{{ include "metallb.secretName" . | quote }}]
+  verbs: ["list"]
+- apiGroups: ["apps"]
+  resources: ["deployments"]
+  resourceNames: ["{{ template "metallb.fullname" . }}-controller"]
+  verbs: ["get"]
+{{- end }}
+- apiGroups: [""]
+  resources: ["secrets"]
+  verbs: ["create", "delete", "get", "list", "patch", "update", "watch"]
+- apiGroups: ["metallb.io"]
+  resources: ["addresspools"]
+  verbs: ["get", "list", "watch"]
+- apiGroups: ["metallb.io"]
+  resources: ["ipaddresspools"]
+  verbs: ["get", "list", "watch"]
+- apiGroups: ["metallb.io"]
+  resources: ["bgppeers"]
+  verbs: ["get", "list"]
+- apiGroups: ["metallb.io"]
+  resources: ["bgpadvertisements"]
+  verbs: ["get", "list"]
+- apiGroups: ["metallb.io"]
+  resources: ["l2advertisements"]
+  verbs: ["get", "list"]
+- apiGroups: ["metallb.io"]
+  resources: ["communities"]
+  verbs: ["get", "list","watch"]
+- apiGroups: ["metallb.io"]
+  resources: ["bfdprofiles"]
+  verbs: ["get", "list","watch"]
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  name: {{ template "metallb.fullname" . }}:controller
+  labels:
+    {{- include "metallb.labels" . | nindent 4 }}
+subjects:
+- kind: ServiceAccount
+  name: {{ template "metallb.controller.serviceAccountName" . }}
+  namespace: {{ .Release.Namespace }}
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: {{ template "metallb.fullname" . }}:controller
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  name: {{ template "metallb.fullname" . }}:speaker
+  labels:
+    {{- include "metallb.labels" . | nindent 4 }}
+subjects:
+- kind: ServiceAccount
+  name: {{ template "metallb.speaker.serviceAccountName" . }}
+  namespace: {{ .Release.Namespace }}
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: {{ template "metallb.fullname" . }}:speaker
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  name: {{ include "metallb.fullname" . }}-pod-lister
+  namespace: {{ .Release.Namespace | quote }}
+  labels: {{- include "metallb.labels" . | nindent 4 }}
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: {{ include "metallb.fullname" . }}-pod-lister
+subjects:
+- kind: ServiceAccount
+  name: {{ include "metallb.speaker.serviceAccountName" . }}
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  name: {{ include "metallb.fullname" . }}-controller
+  namespace: {{ .Release.Namespace | quote }}
+  labels: {{- include "metallb.labels" . | nindent 4 }}
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: {{ include "metallb.fullname" . }}-controller
+subjects:
+- kind: ServiceAccount
+  name: {{ include "metallb.controller.serviceAccountName" . }}
+{{- end -}}
diff --git a/charts/metallb/templates/service-accounts.yaml b/charts/metallb/templates/service-accounts.yaml
new file mode 100644
index 0000000..9615acf
--- /dev/null
+++ b/charts/metallb/templates/service-accounts.yaml
@@ -0,0 +1,30 @@
+{{- if .Values.controller.serviceAccount.create }}
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: {{ template "metallb.controller.serviceAccountName" . }}
+  namespace: {{ .Release.Namespace | quote }}
+  labels:
+    {{- include "metallb.labels" . | nindent 4 }}
+    app.kubernetes.io/component: controller
+  {{- with .Values.controller.serviceAccount.annotations }}
+  annotations:
+    {{- toYaml . | nindent 4 }}
+  {{- end }}
+{{- end }}
+{{- if .Values.speaker.serviceAccount.create }}
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: {{ template "metallb.speaker.serviceAccountName" . }}
+  namespace: {{ .Release.Namespace | quote }}
+  labels:
+    {{- include "metallb.labels" . | nindent 4 }}
+    app.kubernetes.io/component: speaker
+  {{- with .Values.speaker.serviceAccount.annotations }}
+  annotations:
+    {{- toYaml . | nindent 4 }}
+  {{- end }}
+{{- end }}
diff --git a/charts/metallb/templates/servicemonitor.yaml b/charts/metallb/templates/servicemonitor.yaml
new file mode 100644
index 0000000..1cfc0c4
--- /dev/null
+++ b/charts/metallb/templates/servicemonitor.yaml
@@ -0,0 +1,193 @@
+{{- if .Values.prometheus.serviceMonitor.enabled }}
+apiVersion: monitoring.coreos.com/v1
+kind: ServiceMonitor
+metadata:
+  name: {{ template "metallb.fullname" . }}-speaker-monitor
+  namespace: {{ .Release.Namespace | quote }}
+  labels:
+    {{- include "metallb.labels" . | nindent 4 }}
+    app.kubernetes.io/component: speaker
+    {{- if .Values.prometheus.serviceMonitor.speaker.additionalLabels }}
+{{ toYaml .Values.prometheus.serviceMonitor.speaker.additionalLabels | indent 4 }}
+    {{- end }}
+  {{- if .Values.prometheus.serviceMonitor.speaker.annotations }}
+  annotations:
+{{ toYaml .Values.prometheus.serviceMonitor.speaker.annotations | indent 4 }}
+  {{- end }}
+spec:
+  endpoints:
+    - port: {{ template "metrics.exposedportname" . }}
+      honorLabels: true
+      {{- if .Values.prometheus.serviceMonitor.metricRelabelings }}
+      metricRelabelings:
+      {{- toYaml .Values.prometheus.serviceMonitor.metricRelabelings | nindent 8 }}
+      {{- end -}}
+      {{- if .Values.prometheus.serviceMonitor.relabelings }}
+      relabelings:
+      {{- toYaml .Values.prometheus.serviceMonitor.relabelings | nindent 8 }}
+      {{- end }}
+      {{- if .Values.prometheus.serviceMonitor.interval }}
+      interval: {{ .Values.prometheus.serviceMonitor.interval }}
+      {{- end -}}
+{{ if .Values.prometheus.secureMetricsPort }}
+      bearerTokenFile: "/var/run/secrets/kubernetes.io/serviceaccount/token"
+      scheme: "https"
+{{- if .Values.prometheus.serviceMonitor.speaker.tlsConfig }}
+      tlsConfig:
+{{ toYaml .Values.prometheus.serviceMonitor.speaker.tlsConfig | indent 8 }}      
+{{- end }}
+{{ end }}
+{{- if .Values.speaker.frr.enabled }}
+    - port: {{ template "metrics.exposedfrrportname" . }}
+      honorLabels: true
+{{ if .Values.speaker.frr.secureMetricsPort }}
+      {{- if .Values.prometheus.serviceMonitor.interval }}
+      interval: {{ .Values.prometheus.serviceMonitor.interval }}
+      {{- end }}
+      bearerTokenFile: "/var/run/secrets/kubernetes.io/serviceaccount/token"
+      scheme: "https"
+{{- if .Values.prometheus.serviceMonitor.speaker.tlsConfig }}
+      tlsConfig:
+{{ toYaml .Values.prometheus.serviceMonitor.speaker.tlsConfig | indent 8 }}      
+{{- end }}
+{{- end }}
+{{- end }}
+  jobLabel: {{ .Values.prometheus.serviceMonitor.jobLabel | quote }}
+  namespaceSelector:
+    matchNames:
+      - {{ .Release.Namespace }}
+  selector:
+    matchLabels:
+      name: {{ template "metallb.fullname" . }}-speaker-monitor-service
+---
+apiVersion: v1
+kind: Service
+metadata:
+  annotations:
+    prometheus.io/scrape: "true"
+  {{- if .Values.prometheus.serviceMonitor.speaker.annotations }}
+{{ toYaml .Values.prometheus.serviceMonitor.speaker.annotations | indent 4 }}
+  {{- end }}
+  labels:
+    name: {{ template "metallb.fullname" . }}-speaker-monitor-service
+  name: {{ template "metallb.fullname" . }}-speaker-monitor-service
+  namespace: {{ .Release.Namespace | quote }}
+spec:
+  selector:
+    {{- include "metallb.selectorLabels" . | nindent 4 }}
+    app.kubernetes.io/component: speaker
+  clusterIP: None
+  ports:
+    - name: {{ template "metrics.exposedportname" . }}
+      port: {{ template "metrics.exposedport" . }}
+      targetPort: {{ template "metrics.exposedport" . }}
+{{- if .Values.speaker.frr.enabled }}
+    - name: {{ template "metrics.exposedfrrportname" . }}
+      port: {{ template "metrics.exposedfrrport" . }}
+      targetPort: {{ template "metrics.exposedfrrport" . }}
+{{- end }}
+  sessionAffinity: None
+  type: ClusterIP
+---
+apiVersion: monitoring.coreos.com/v1
+kind: ServiceMonitor
+metadata:
+  name: {{ template "metallb.fullname" . }}-controller-monitor
+  namespace: {{ .Release.Namespace | quote }}
+  labels:
+    {{- include "metallb.labels" . | nindent 4 }}
+    app.kubernetes.io/component: speaker
+    {{- if .Values.prometheus.serviceMonitor.controller.additionalLabels }}
+{{ toYaml .Values.prometheus.serviceMonitor.controller.additionalLabels | indent 4 }}
+    {{- end }}
+  {{- if .Values.prometheus.serviceMonitor.controller.annotations }}
+  annotations:
+{{ toYaml .Values.prometheus.serviceMonitor.controller.annotations | indent 4 }}
+  {{- end }}
+spec:
+  endpoints:
+    - port: {{ template "metrics.exposedportname" . }}
+      {{- if .Values.prometheus.serviceMonitor.metricRelabelings }}
+      metricRelabelings:
+      {{- toYaml .Values.prometheus.serviceMonitor.metricRelabelings | nindent 8 }}
+      {{- end -}}
+      {{- if .Values.prometheus.serviceMonitor.relabelings }}
+      relabelings:
+      {{- toYaml .Values.prometheus.serviceMonitor.relabelings | nindent 8 }}
+      {{- end }}
+      {{- if .Values.prometheus.serviceMonitor.interval }}
+      interval: {{ .Values.prometheus.serviceMonitor.interval }}
+      {{- end }}
+      honorLabels: true
+{{- if .Values.prometheus.secureMetricsPort }}
+      bearerTokenFile: "/var/run/secrets/kubernetes.io/serviceaccount/token"
+      scheme: "https"
+{{- if .Values.prometheus.serviceMonitor.controller.tlsConfig }}
+      tlsConfig:
+{{ toYaml .Values.prometheus.serviceMonitor.controller.tlsConfig | indent 8 }}      
+{{- end }}
+{{- end }}
+  jobLabel: {{ .Values.prometheus.serviceMonitor.jobLabel | quote }}
+  namespaceSelector:
+    matchNames:
+      - {{ .Release.Namespace }}
+  selector:
+    matchLabels:
+      name: {{ template "metallb.fullname" . }}-controller-monitor-service
+---
+apiVersion: v1
+kind: Service
+metadata:
+  annotations:
+    prometheus.io/scrape: "true"
+  {{- if .Values.prometheus.serviceMonitor.controller.annotations }}
+{{ toYaml .Values.prometheus.serviceMonitor.controller.annotations | indent 4 }}
+  {{- end }}
+  labels:
+    name: {{ template "metallb.fullname" . }}-controller-monitor-service
+  name: {{ template "metallb.fullname" . }}-controller-monitor-service
+spec:
+  selector:
+    {{- include "metallb.selectorLabels" . | nindent 4 }}
+    app.kubernetes.io/component: controller
+  clusterIP: None
+  ports:
+    - name: {{ template "metrics.exposedportname" . }}
+      port: {{ template "metrics.exposedport" . }}
+      targetPort: {{ template "metrics.exposedport" . }}
+  sessionAffinity: None
+  type: ClusterIP
+---
+{{- if .Values.prometheus.rbacPrometheus }}
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: {{ template "metallb.fullname" . }}-prometheus
+  namespace: {{ .Release.Namespace | quote }}
+rules:
+  - apiGroups:
+      - ""
+    resources:
+      - pods
+      - services
+      - endpoints
+    verbs:
+      - get
+      - list
+      - watch
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  name: {{ template "metallb.fullname" . }}-prometheus
+  namespace: {{ .Release.Namespace | quote }}
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: {{ template "metallb.fullname" . }}-prometheus
+subjects:
+  - kind: ServiceAccount
+    name: {{ required ".Values.prometheus.serviceAccount must be defined when .Values.prometheus.serviceMonitor.enabled == true" .Values.prometheus.serviceAccount }}
+    namespace: {{ required ".Values.prometheus.namespace must be defined when .Values.prometheus.serviceMonitor.enabled == true" .Values.prometheus.namespace }}
+{{- end }}
+{{- end }}
diff --git a/charts/metallb/templates/speaker.yaml b/charts/metallb/templates/speaker.yaml
new file mode 100644
index 0000000..1a4c7b2
--- /dev/null
+++ b/charts/metallb/templates/speaker.yaml
@@ -0,0 +1,510 @@
+{{- if .Values.speaker.frr.enabled }}
+# FRR expects to have these files owned by frr:frr on startup.
+# Having them in a ConfigMap allows us to modify behaviors: for example enabling more daemons on startup.
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: {{ template "metallb.fullname" . }}-frr-startup
+  namespace: {{ .Release.Namespace | quote }}
+  labels:
+    {{- include "metallb.labels" . | nindent 4 }}
+    app.kubernetes.io/component: speaker
+data:
+  daemons: |
+    # This file tells the frr package which daemons to start.
+    #
+    # Sample configurations for these daemons can be found in
+    # /usr/share/doc/frr/examples/.
+    #
+    # ATTENTION:
+    #
+    # When activating a daemon for the first time, a config file, even if it is
+    # empty, has to be present *and* be owned by the user and group "frr", else
+    # the daemon will not be started by /etc/init.d/frr. The permissions should
+    # be u=rw,g=r,o=.
+    # When using "vtysh" such a config file is also needed. It should be owned by
+    # group "frrvty" and set to ug=rw,o= though. Check /etc/pam.d/frr, too.
+    #
+    # The watchfrr and zebra daemons are always started.
+    #
+    bgpd=yes
+    ospfd=no
+    ospf6d=no
+    ripd=no
+    ripngd=no
+    isisd=no
+    pimd=no
+    ldpd=no
+    nhrpd=no
+    eigrpd=no
+    babeld=no
+    sharpd=no
+    pbrd=no
+    bfdd=yes
+    fabricd=no
+    vrrpd=no
+
+    #
+    # If this option is set the /etc/init.d/frr script automatically loads
+    # the config via "vtysh -b" when the servers are started.
+    # Check /etc/pam.d/frr if you intend to use "vtysh"!
+    #
+    vtysh_enable=yes
+    zebra_options="  -A 127.0.0.1 -s 90000000"
+    bgpd_options="   -A 127.0.0.1 -p 0"
+    ospfd_options="  -A 127.0.0.1"
+    ospf6d_options=" -A ::1"
+    ripd_options="   -A 127.0.0.1"
+    ripngd_options=" -A ::1"
+    isisd_options="  -A 127.0.0.1"
+    pimd_options="   -A 127.0.0.1"
+    ldpd_options="   -A 127.0.0.1"
+    nhrpd_options="  -A 127.0.0.1"
+    eigrpd_options=" -A 127.0.0.1"
+    babeld_options=" -A 127.0.0.1"
+    sharpd_options=" -A 127.0.0.1"
+    pbrd_options="   -A 127.0.0.1"
+    staticd_options="-A 127.0.0.1"
+    bfdd_options="   -A 127.0.0.1"
+    fabricd_options="-A 127.0.0.1"
+    vrrpd_options="  -A 127.0.0.1"
+
+    # configuration profile
+    #
+    #frr_profile="traditional"
+    #frr_profile="datacenter"
+
+    #
+    # This is the maximum number of FD's that will be available.
+    # Upon startup this is read by the control files and ulimit
+    # is called. Uncomment and use a reasonable value for your
+    # setup if you are expecting a large number of peers in
+    # say BGP.
+    #MAX_FDS=1024
+
+    # The list of daemons to watch is automatically generated by the init script.
+    #watchfrr_options=""
+
+    # for debugging purposes, you can specify a "wrap" command to start instead
+    # of starting the daemon directly, e.g. to use valgrind on ospfd:
+    #   ospfd_wrap="/usr/bin/valgrind"
+    # or you can use "all_wrap" for all daemons, e.g. to use perf record:
+    #   all_wrap="/usr/bin/perf record --call-graph -"
+    # the normal daemon command is added to this at the end.
+  vtysh.conf: |+
+    service integrated-vtysh-config
+  frr.conf: |+
+    ! This file gets overriden the first time the speaker renders a config.
+    ! So anything configured here is only temporary.
+    frr version 7.5.1
+    frr defaults traditional
+    hostname Router
+    line vty
+    log file /etc/frr/frr.log informational
+{{- end }}
+---
+{{- if .Values.speaker.enabled }}
+apiVersion: apps/v1
+kind: DaemonSet
+metadata:
+  name: {{ template "metallb.fullname" . }}-speaker
+  namespace: {{ .Release.Namespace | quote }}
+  labels:
+    {{- include "metallb.labels" . | nindent 4 }}
+    app.kubernetes.io/component: speaker
+    {{- range $key, $value := .Values.speaker.labels }}
+    {{ $key }}: {{ $value | quote }}
+    {{- end }}
+spec:
+  {{- if .Values.speaker.updateStrategy }}
+  updateStrategy: {{- toYaml .Values.speaker.updateStrategy | nindent 4 }}
+  {{- end }}
+  selector:
+    matchLabels:
+      {{- include "metallb.selectorLabels" . | nindent 6 }}
+      app.kubernetes.io/component: speaker
+  template:
+    metadata:
+      {{- if or .Values.prometheus.scrapeAnnotations .Values.speaker.podAnnotations }}
+      annotations:
+        {{- if .Values.prometheus.scrapeAnnotations }}
+        prometheus.io/scrape: "true"
+        {{- if not .Values.speaker.frr.enabled }}
+        prometheus.io/port: "{{ .Values.prometheus.metricsPort }}"
+        {{- end }}
+        {{- end }}
+        {{- with .Values.speaker.podAnnotations }}
+          {{- toYaml . | nindent 8 }}
+        {{- end }}
+      {{- end }}
+      labels:
+        {{- include "metallb.selectorLabels" . | nindent 8 }}
+        app.kubernetes.io/component: speaker
+        {{- range $key, $value := .Values.speaker.labels }}
+        {{ $key }}: {{ $value | quote }}
+        {{- end }}
+    spec:
+      {{- if .Values.speaker.runtimeClassName }}
+      runtimeClassName: {{ .Values.speaker.runtimeClassName }}
+      {{- end }}
+      {{- with .Values.imagePullSecrets }}
+      imagePullSecrets:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      serviceAccountName: {{ template "metallb.speaker.serviceAccountName" . }}
+      terminationGracePeriodSeconds: 0
+      hostNetwork: true
+      volumes:
+      {{- if .Values.speaker.memberlist.enabled }}
+        - name: memberlist
+          secret:
+            secretName: {{ include "metallb.secretName" . }}
+            defaultMode: 420
+      {{- end }}
+      {{- if .Values.speaker.excludeInterfaces.enabled }}
+        - name: metallb-excludel2
+          configMap:
+            defaultMode: 256
+            name: metallb-excludel2
+      {{- end }}
+      {{- if .Values.speaker.frr.enabled }}
+        - name: frr-sockets
+          emptyDir: {}
+        - name: frr-startup
+          configMap:
+            name: {{ template "metallb.fullname" . }}-frr-startup
+        - name: frr-conf
+          emptyDir: {}
+        - name: reloader
+          emptyDir: {}
+        - name: metrics
+          emptyDir: {}
+      {{- if .Values.prometheus.speakerMetricsTLSSecret }}
+        - name: metrics-certs
+          secret:
+            secretName: {{ .Values.prometheus.speakerMetricsTLSSecret }}
+      {{- end }}
+      initContainers:
+        # Copies the initial config files with the right permissions to the shared volume.
+        - name: cp-frr-files
+          image: {{ .Values.speaker.frr.image.repository }}:{{ .Values.speaker.frr.image.tag | default .Chart.AppVersion }}
+          securityContext:
+            runAsUser: 100
+            runAsGroup: 101
+          command: ["/bin/sh", "-c", "cp -rLf /tmp/frr/* /etc/frr/"]
+          volumeMounts:
+            - name: frr-startup
+              mountPath: /tmp/frr
+            - name: frr-conf
+              mountPath: /etc/frr
+        # Copies the reloader to the shared volume between the speaker and reloader.
+        - name: cp-reloader
+          image: {{ .Values.speaker.image.repository }}:{{ .Values.speaker.image.tag | default .Chart.AppVersion }}
+          command: ["/bin/sh", "-c", "cp -f /frr-reloader.sh /etc/frr_reloader/"]
+          volumeMounts:
+            - name: reloader
+              mountPath: /etc/frr_reloader
+        # Copies the metrics exporter
+        - name: cp-metrics
+          image: {{ .Values.speaker.image.repository }}:{{ .Values.speaker.image.tag | default .Chart.AppVersion }}
+          command: ["/bin/sh", "-c", "cp -f /frr-metrics /etc/frr_metrics/"]
+          volumeMounts:
+            - name: metrics
+              mountPath: /etc/frr_metrics
+      shareProcessNamespace: true
+      {{- end }}
+      containers:
+      - name: speaker
+        image: {{ .Values.speaker.image.repository }}:{{ .Values.speaker.image.tag | default .Chart.AppVersion }}
+        {{- if .Values.speaker.image.pullPolicy }}
+        imagePullPolicy: {{ .Values.speaker.image.pullPolicy }}
+        {{- end }}
+        {{- if .Values.speaker.command }}
+        command:
+          - {{ .Values.speaker.command }}
+        {{- end }}
+        args:
+        - --port={{ .Values.prometheus.metricsPort }}
+        {{- with .Values.speaker.logLevel }}
+        - --log-level={{ . }}
+        {{- end }}
+        {{- if .Values.loadBalancerClass }}
+        - --lb-class={{ .Values.loadBalancerClass }}
+        {{- end }}
+        env:
+        - name: METALLB_NODE_NAME
+          valueFrom:
+            fieldRef:
+              fieldPath: spec.nodeName
+        - name: METALLB_HOST
+          valueFrom:
+            fieldRef:
+              fieldPath: status.hostIP
+        {{- if .Values.speaker.memberlist.enabled }}
+        - name: METALLB_ML_BIND_ADDR
+          valueFrom:
+            fieldRef:
+              fieldPath: status.podIP
+        - name: METALLB_ML_LABELS
+          value: "app.kubernetes.io/name={{ include "metallb.name" . }},app.kubernetes.io/component=speaker"
+        - name: METALLB_ML_BIND_PORT
+          value: "{{ .Values.speaker.memberlist.mlBindPort }}"
+        - name: METALLB_ML_SECRET_KEY_PATH
+          value: "{{ .Values.speaker.memberlist.mlSecretKeyPath }}"
+        {{- end }}
+        {{- if .Values.speaker.frr.enabled }}
+        - name: FRR_CONFIG_FILE
+          value: /etc/frr_reloader/frr.conf
+        - name: FRR_RELOADER_PID_FILE
+          value: /etc/frr_reloader/reloader.pid
+        - name: METALLB_BGP_TYPE
+          value: frr
+        {{- end }}
+        ports:
+        - name: monitoring
+          containerPort: {{ .Values.prometheus.metricsPort }}
+        {{- if .Values.speaker.memberlist.enabled }}
+        - name: memberlist-tcp
+          containerPort: {{ .Values.speaker.memberlist.mlBindPort }}
+          protocol: TCP
+        - name: memberlist-udp
+          containerPort: {{ .Values.speaker.memberlist.mlBindPort }}
+          protocol: UDP
+        {{- end }}
+        {{- if .Values.speaker.livenessProbe.enabled }}
+        livenessProbe:
+          httpGet:
+            path: /metrics
+            port: monitoring
+          initialDelaySeconds: {{ .Values.speaker.livenessProbe.initialDelaySeconds }}
+          periodSeconds: {{ .Values.speaker.livenessProbe.periodSeconds }}
+          timeoutSeconds: {{ .Values.speaker.livenessProbe.timeoutSeconds }}
+          successThreshold: {{ .Values.speaker.livenessProbe.successThreshold }}
+          failureThreshold: {{ .Values.speaker.livenessProbe.failureThreshold }}
+        {{- end }}
+        {{- if .Values.speaker.readinessProbe.enabled }}
+        readinessProbe:
+          httpGet:
+            path: /metrics
+            port: monitoring
+          initialDelaySeconds: {{ .Values.speaker.readinessProbe.initialDelaySeconds }}
+          periodSeconds: {{ .Values.speaker.readinessProbe.periodSeconds }}
+          timeoutSeconds: {{ .Values.speaker.readinessProbe.timeoutSeconds }}
+          successThreshold: {{ .Values.speaker.readinessProbe.successThreshold }}
+          failureThreshold: {{ .Values.speaker.readinessProbe.failureThreshold }}
+        {{- end }}
+        {{- with .Values.speaker.resources }}
+        resources:
+          {{- toYaml . | nindent 10 }}
+        {{- end }}
+        securityContext:
+          allowPrivilegeEscalation: false
+          readOnlyRootFilesystem: true
+          capabilities:
+            drop:
+            - ALL
+            add:
+            - NET_RAW
+        {{- if or .Values.speaker.frr.enabled .Values.speaker.memberlist.enabled .Values.speaker.excludeInterfaces.enabled }}
+        volumeMounts:
+          {{- if .Values.speaker.memberlist.enabled }}
+          - name: memberlist 
+            mountPath: {{ .Values.speaker.memberlist.mlSecretKeyPath }}
+          {{- end }}
+          {{- if .Values.speaker.frr.enabled }}
+          - name: reloader
+            mountPath: /etc/frr_reloader
+          {{- end }}
+          {{- if .Values.speaker.excludeInterfaces.enabled }}
+          - name: metallb-excludel2
+            mountPath: /etc/metallb
+          {{- end }}
+        {{- end }}
+      {{- if .Values.speaker.frr.enabled }}
+      - name: frr
+        securityContext:
+          capabilities:
+            add:
+            - NET_ADMIN
+            - NET_RAW
+            - SYS_ADMIN
+            - NET_BIND_SERVICE
+        image: {{ .Values.speaker.frr.image.repository }}:{{ .Values.speaker.frr.image.tag | default .Chart.AppVersion }}
+        {{- if .Values.speaker.frr.image.pullPolicy }}
+        imagePullPolicy: {{ .Values.speaker.frr.image.pullPolicy }}
+        {{- end }}
+        env:
+          - name: TINI_SUBREAPER
+            value: "true"
+        volumeMounts:
+          - name: frr-sockets
+            mountPath: /var/run/frr
+          - name: frr-conf
+            mountPath: /etc/frr
+        # The command is FRR's default entrypoint & waiting for the log file to appear and tailing it.
+        # If the log file isn't created in 60 seconds the tail fails and the container is restarted.
+        # This workaround is needed to have the frr logs as part of kubectl logs -c frr < speaker_pod_name >.
+        command:
+          - /bin/sh
+          - -c
+          - |
+            /sbin/tini -- /usr/lib/frr/docker-start &
+            attempts=0
+            until [[ -f /etc/frr/frr.log || $attempts -eq 60 ]]; do
+              sleep 1
+              attempts=$(( $attempts + 1 ))
+            done
+            tail -f /etc/frr/frr.log
+        {{- with .Values.speaker.frr.resources }}
+        resources:
+          {{- toYaml . | nindent 12 }}
+        {{- end }}
+        {{- if .Values.speaker.livenessProbe.enabled }}
+        livenessProbe:
+          httpGet:
+            path: /livez
+            port: {{ .Values.speaker.frr.metricsPort }}
+          initialDelaySeconds: {{ .Values.speaker.livenessProbe.initialDelaySeconds }}
+          periodSeconds: {{ .Values.speaker.livenessProbe.periodSeconds }}
+          timeoutSeconds: {{ .Values.speaker.livenessProbe.timeoutSeconds }}
+          successThreshold: {{ .Values.speaker.livenessProbe.successThreshold }}
+          failureThreshold: {{ .Values.speaker.livenessProbe.failureThreshold }}
+        {{- end }}
+        {{- if .Values.speaker.startupProbe.enabled }}
+        startupProbe:
+          httpGet:
+            path: /livez
+            port: {{ .Values.speaker.frr.metricsPort }}
+          failureThreshold: {{ .Values.speaker.startupProbe.failureThreshold }}
+          periodSeconds: {{ .Values.speaker.startupProbe.periodSeconds }}
+        {{- end }}
+      - name: reloader
+        image: {{ .Values.speaker.frr.image.repository }}:{{ .Values.speaker.frr.image.tag | default .Chart.AppVersion }}
+        {{- if .Values.speaker.frr.image.pullPolicy }}
+        imagePullPolicy: {{ .Values.speaker.frr.image.pullPolicy }}
+        {{- end }}
+        command: ["/etc/frr_reloader/frr-reloader.sh"]
+        volumeMounts:
+          - name: frr-sockets
+            mountPath: /var/run/frr
+          - name: frr-conf
+            mountPath: /etc/frr
+          - name: reloader
+            mountPath: /etc/frr_reloader
+        {{- with .Values.speaker.reloader.resources }}
+        resources:
+          {{- toYaml . | nindent 12 }}
+        {{- end }}
+      - name: frr-metrics
+        image: {{ .Values.speaker.frr.image.repository }}:{{ .Values.speaker.frr.image.tag | default .Chart.AppVersion }}
+        command: ["/etc/frr_metrics/frr-metrics"]
+        args:
+          - --metrics-port={{ .Values.speaker.frr.metricsPort }}
+        ports:
+          - containerPort: {{ .Values.speaker.frr.metricsPort }}
+            name: monitoring
+        volumeMounts:
+          - name: frr-sockets
+            mountPath: /var/run/frr
+          - name: frr-conf
+            mountPath: /etc/frr
+          - name: metrics
+            mountPath: /etc/frr_metrics
+        {{- with .Values.speaker.frrMetrics.resources }}
+        resources:
+          {{- toYaml . | nindent 12 }}
+        {{- end }}
+      {{- end }}
+      {{- if .Values.prometheus.secureMetricsPort }}
+      - name: kube-rbac-proxy
+        image: {{ .Values.prometheus.rbacProxy.repository }}:{{ .Values.prometheus.rbacProxy.tag }}
+        imagePullPolicy: {{ .Values.prometheus.rbacProxy.pullPolicy }}
+        args:
+          - --logtostderr
+          - --secure-listen-address=:{{ .Values.prometheus.secureMetricsPort }}
+          - --upstream=http://$(METALLB_HOST):{{ .Values.prometheus.metricsPort }}/
+          - --tls-cipher-suites=TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_RSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256
+        {{- if .Values.prometheus.speakerMetricsTLSSecret }}
+          - --tls-private-key-file=/etc/metrics/tls.key
+          - --tls-cert-file=/etc/metrics/tls.crt
+        {{- end }}
+        ports:
+          - containerPort: {{ .Values.prometheus.secureMetricsPort }}
+            name: metricshttps
+        env:
+          - name: METALLB_HOST
+            valueFrom:
+              fieldRef:
+                fieldPath: status.hostIP
+        resources:
+          requests:
+            cpu: 10m
+            memory: 20Mi
+        terminationMessagePolicy: FallbackToLogsOnError
+        {{- if .Values.prometheus.speakerMetricsTLSSecret }}
+        volumeMounts:
+          - name: metrics-certs
+            mountPath: /etc/metrics
+            readOnly: true
+        {{- end }}
+      {{ end }}
+      {{- if .Values.speaker.frr.secureMetricsPort }}
+      - name: kube-rbac-proxy-frr
+        image: {{ .Values.prometheus.rbacProxy.repository }}:{{ .Values.prometheus.rbacProxy.tag | default .Chart.AppVersion }}
+        imagePullPolicy: {{ .Values.prometheus.rbacProxy.pullPolicy }}
+        args:
+          - --logtostderr
+          - --secure-listen-address=:{{ .Values.speaker.frr.secureMetricsPort }}
+          - --tls-cipher-suites=TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_RSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256
+          - --upstream=http://$(METALLB_HOST):{{ .Values.speaker.frr.metricsPort }}/
+        {{- if .Values.prometheus.speakerMetricsTLSSecret }}
+          - --tls-private-key-file=/etc/metrics/tls.key
+          - --tls-cert-file=/etc/metrics/tls.crt
+        {{- end }}
+        ports:
+          - containerPort: {{ .Values.speaker.frr.secureMetricsPort }}
+            name: metricshttps
+        env:
+          - name: METALLB_HOST
+            valueFrom:
+              fieldRef:
+                fieldPath: status.hostIP
+        resources:
+          requests:
+            cpu: 10m
+            memory: 20Mi
+        terminationMessagePolicy: FallbackToLogsOnError
+        {{- if .Values.prometheus.speakerMetricsTLSSecret }}
+        volumeMounts:
+          - name: metrics-certs
+            mountPath: /etc/metrics
+            readOnly: true
+        {{- end }}
+      {{ end }}
+      nodeSelector:
+        "kubernetes.io/os": linux
+        {{- with .Values.speaker.nodeSelector }}
+          {{- toYaml . | nindent 8 }}
+        {{- end }}
+      {{- with .Values.speaker.affinity }}
+      affinity:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      {{- if or .Values.speaker.tolerateMaster .Values.speaker.tolerations }}
+      tolerations:
+      {{- if .Values.speaker.tolerateMaster }}
+      - key: node-role.kubernetes.io/master
+        effect: NoSchedule
+        operator: Exists
+      - key: node-role.kubernetes.io/control-plane
+        effect: NoSchedule
+        operator: Exists        
+      {{- end }}
+      {{- with .Values.speaker.tolerations }}
+        {{- toYaml . | nindent 6 }}
+      {{- end }}
+      {{- end }}
+      {{- with .Values.speaker.priorityClassName }}
+      priorityClassName: {{ . | quote }}
+      {{- end }}
+{{- end }}
diff --git a/charts/metallb/templates/webhooks.yaml b/charts/metallb/templates/webhooks.yaml
new file mode 100644
index 0000000..3b587a4
--- /dev/null
+++ b/charts/metallb/templates/webhooks.yaml
@@ -0,0 +1,170 @@
+apiVersion: admissionregistration.k8s.io/v1
+kind: ValidatingWebhookConfiguration
+metadata:
+  name: metallb-webhook-configuration
+  labels:
+    {{- include "metallb.labels" . | nindent 4 }}
+webhooks:
+- admissionReviewVersions:
+  - v1
+  clientConfig:
+    service:
+      name: metallb-webhook-service
+      namespace: {{ .Release.Namespace }}
+      path: /validate-metallb-io-v1beta1-addresspool
+  failurePolicy: {{ .Values.crds.validationFailurePolicy }}
+  name: addresspoolvalidationwebhook.metallb.io
+  rules:
+  - apiGroups:
+    - metallb.io
+    apiVersions:
+    - v1beta1
+    operations:
+    - CREATE
+    - UPDATE
+    resources:
+    - addresspools
+  sideEffects: None
+- admissionReviewVersions:
+  - v1
+  clientConfig:
+    service:
+      name: metallb-webhook-service
+      namespace: {{ .Release.Namespace }}
+      path: /validate-metallb-io-v1beta2-bgppeer
+  failurePolicy: {{ .Values.crds.validationFailurePolicy }}
+  name: bgppeervalidationwebhook.metallb.io
+  rules:
+  - apiGroups:
+    - metallb.io
+    apiVersions:
+    - v1beta2
+    operations:
+    - CREATE
+    - UPDATE
+    resources:
+    - bgppeers
+  sideEffects: None
+- admissionReviewVersions:
+  - v1
+  clientConfig:
+    service:
+      name: metallb-webhook-service
+      namespace: {{ .Release.Namespace }}
+      path: /validate-metallb-io-v1beta1-ipaddresspool
+  failurePolicy: {{ .Values.crds.validationFailurePolicy }}
+  name: ipaddresspoolvalidationwebhook.metallb.io
+  rules:
+  - apiGroups:
+    - metallb.io
+    apiVersions:
+    - v1beta1
+    operations:
+    - CREATE
+    - UPDATE
+    resources:
+    - ipaddresspools
+  sideEffects: None
+- admissionReviewVersions:
+  - v1
+  clientConfig:
+    service:
+      name: metallb-webhook-service
+      namespace: {{ .Release.Namespace }}
+      path: /validate-metallb-io-v1beta1-bgpadvertisement
+  failurePolicy: {{ .Values.crds.validationFailurePolicy }}
+  name: bgpadvertisementvalidationwebhook.metallb.io
+  rules:
+  - apiGroups:
+    - metallb.io
+    apiVersions:
+    - v1beta1
+    operations:
+    - CREATE
+    - UPDATE
+    resources:
+    - bgpadvertisements
+  sideEffects: None
+- admissionReviewVersions:
+  - v1
+  clientConfig:
+    service:
+      name: metallb-webhook-service
+      namespace: {{ .Release.Namespace }}
+      path: /validate-metallb-io-v1beta1-community
+  failurePolicy: {{ .Values.crds.validationFailurePolicy }}
+  name: communityvalidationwebhook.metallb.io
+  rules:
+  - apiGroups:
+    - metallb.io
+    apiVersions:
+    - v1beta1
+    operations:
+    - CREATE
+    - UPDATE
+    resources:
+    - communities
+  sideEffects: None
+- admissionReviewVersions:
+  - v1
+  clientConfig:
+    service:
+      name: metallb-webhook-service
+      namespace: {{ .Release.Namespace }}
+      path: /validate-metallb-io-v1beta1-bfdprofile
+  failurePolicy: {{ .Values.crds.validationFailurePolicy }}
+  name: bfdprofilevalidationwebhook.metallb.io
+  rules:
+  - apiGroups:
+    - metallb.io
+    apiVersions:
+    - v1beta1
+    operations:
+    - CREATE
+    - DELETE
+    resources:
+    - bfdprofiles
+  sideEffects: None
+- admissionReviewVersions:
+  - v1
+  clientConfig:
+    service:
+      name: metallb-webhook-service
+      namespace: {{ .Release.Namespace }}
+      path: /validate-metallb-io-v1beta1-l2advertisement
+  failurePolicy: {{ .Values.crds.validationFailurePolicy }}
+  name: l2advertisementvalidationwebhook.metallb.io
+  rules:
+  - apiGroups:
+    - metallb.io
+    apiVersions:
+    - v1beta1
+    operations:
+    - CREATE
+    - UPDATE
+    resources:
+    - l2advertisements
+  sideEffects: None
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: metallb-webhook-service
+  namespace: {{ .Release.Namespace | quote }}
+  labels:
+    {{- include "metallb.labels" . | nindent 4 }}
+spec:
+  ports:
+  - port: 443
+    targetPort: 9443
+  selector:
+    {{- include "metallb.selectorLabels" . | nindent 4 }}
+    app.kubernetes.io/component: controller
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: webhook-server-cert
+  namespace: {{ .Release.Namespace | quote }}
+  labels:
+    {{- include "metallb.labels" . | nindent 4 }}
diff --git a/charts/metallb/values.schema.json b/charts/metallb/values.schema.json
new file mode 100644
index 0000000..5a92e56
--- /dev/null
+++ b/charts/metallb/values.schema.json
@@ -0,0 +1,427 @@
+{
+  "$schema": "https://json-schema.org/draft-07/schema#",
+  "title": "Values",
+  "type": "object",
+  "definitions": {
+    "prometheusAlert": {
+      "type": "object",
+      "properties": {
+        "enabled": {
+          "type": "boolean"
+        },
+        "labels": {
+          "type": "object",
+          "additionalProperties": { "type": "string" }
+        }
+      },
+      "required": [ "enabled" ]
+    },
+    "probe": {
+      "type": "object",
+      "properties": {
+        "enabled": {
+          "type": "boolean"
+        },
+        "failureThreshold": {
+          "type": "integer"
+        },
+        "initialDelaySeconds": {
+          "type": "integer"
+        },
+        "periodSeconds": {
+          "type": "integer"
+        },
+        "successThreshold": {
+          "type": "integer"
+        },
+        "timeoutSeconds": {
+          "type": "integer"
+        }
+      },
+      "required": [
+        "failureThreshold",
+        "initialDelaySeconds",
+        "periodSeconds",
+        "successThreshold",
+        "timeoutSeconds"
+      ]
+    },
+    "component": {
+      "type": "object",
+      "properties": {
+        "enabled": {
+          "type": "boolean"
+        },
+        "logLevel": {
+          "type": "string",
+          "enum": [ "all", "debug", "info", "warn", "error", "none" ]
+        },
+        "image": {
+          "type": "object",
+          "properties": {
+            "repository": {
+              "type": "string"
+            },
+            "tag": {
+              "anyOf": [
+                { "type": "string" },
+                { "type": "null" }
+              ]
+            },
+            "pullPolicy": {
+              "anyOf": [
+                {
+                  "type": "null"
+                },
+                {
+                  "type": "string",
+                  "enum": [ "Always", "IfNotPresent", "Never" ]
+                }
+              ]
+            }
+          }
+        },
+        "serviceAccount": {
+          "type": "object",
+          "properties": {
+            "create": {
+              "type": "boolean"
+            },
+            "name": {
+              "type": "string"
+            },
+            "annotations": {
+              "type": "object"
+            }
+          }
+        },
+        "resources": {
+          "type": "object"
+        },
+        "nodeSelector": {
+          "type": "object"
+        },
+        "tolerations": {
+          "type": "array",
+          "items": {
+            "type": "object"
+          }
+        },
+        "priorityClassName": {
+          "type":"string"
+        },
+        "runtimeClassName": {
+          "type":"string"
+        },
+        "affinity": {
+          "type": "object"
+        },
+        "podAnnotations": {
+          "type": "object"
+        },
+        "livenessProbe": {
+          "$ref": "#/definitions/probe"
+        },
+        "readinessProbe": {
+          "$ref": "#/definitions/probe"
+        }
+      },
+      "required": [
+        "image",
+        "serviceAccount"
+      ]
+    }
+  },
+  "properties": {
+    "imagePullSecrets": {
+      "description": "Secrets used for pulling images",
+      "type": "array",
+      "items": {
+        "type": "object",
+        "properties": {
+          "name": {
+            "type": "string"
+          }
+        },
+        "required": [ "name" ],
+        "additionalProperties": false
+      }
+    },
+    "nameOverride": {
+      "description": "Override chart name",
+      "type": "string"
+    },
+    "fullNameOverride": {
+      "description": "Override fully qualified app name",
+      "type": "string"
+    },
+    "configInLine": {
+      "description": "MetalLB configuration",
+      "type": "object"
+    },
+    "loadBalancerClass": {
+      "type":"string"
+    },
+    "rbac": {
+      "description": "RBAC configuration",
+      "type": "object",
+      "properties": {
+        "create": {
+          "description": "Enable RBAC",
+          "type": "boolean"
+        }
+      }
+    },
+    "prometheus": {
+      "description": "Prometheus monitoring config",
+      "type": "object",
+      "properties": {
+        "scrapeAnnotations": { "type": "boolean" },
+        "metricsPort": { "type": "integer" },
+        "secureMetricsPort": { "type": "integer" },
+        "rbacPrometheus": { "type": "boolean" },
+        "serviceAccount": { "type": "string" },
+        "namespace": { "type": "string" },
+        "rbacProxy": {
+          "description": "kube-rbac-proxy configuration",
+          "type": "object",
+          "properties": {
+            "repository": { "type": "string" },
+            "tag": { "type": "string" }
+          }
+        },
+        "podMonitor": {
+          "description": "Prometheus Operator PodMonitors",
+          "type": "object",
+          "properties": {
+            "enabled": { "type": "boolean" },
+            "additionalMonitors": { "type": "object" },
+            "jobLabel": { "type": "string" },
+            "interval": {
+              "anyOf": [
+                { "type": "integer" },
+                { "type": "null" }
+              ]
+            },
+            "metricRelabelings": {
+              "type": "array",
+              "items": {
+                "type": "object"
+              }
+            },
+            "relabelings": {
+              "type": "array",
+              "items": {
+                "type": "object"
+              }
+            }
+          }
+        },
+        "serviceMonitor": {
+          "description": "Prometheus Operator ServiceMonitors",
+          "type": "object",
+          "properties": {
+            "enabled": { "type": "boolean" },
+            "jobLabel": { "type": "string" },
+            "interval": {
+              "anyOf": [
+                { "type": "integer" },
+                { "type": "null" }
+              ]
+            },
+            "metricRelabelings": {
+              "type": "array",
+              "items": {
+                "type": "object"
+              }
+            },
+            "relabelings": {
+              "type": "array",
+              "items": {
+                "type": "object"
+              }
+            }
+          }
+        },
+        "prometheusRule": {
+          "description": "Prometheus Operator alertmanager alerts",
+          "type": "object",
+          "properties": {
+            "enabled": { "type": "boolean" },
+            "additionalMonitors": { "type": "object" },
+            "staleConfig": { "$ref": "#/definitions/prometheusAlert" },
+            "configNotLoaded": { "$ref": "#/definitions/prometheusAlert" },
+            "addressPoolExhausted": { "$ref": "#/definitions/prometheusAlert" },
+            "addressPoolUsage": {
+              "type": "object",
+              "properties": {
+                "enabled": {
+                  "type": "boolean"
+                },
+                "thresholds": {
+                  "type": "array",
+                  "items": {
+                    "type": "object",
+                    "properties": {
+                      "percent": {
+                        "type": "integer",
+                        "minimum": 0,
+                        "maximum": 100
+                      },
+                      "labels": {
+                        "type": "object",
+                        "additionalProperties": { "type": "string" }
+                      }
+                    },
+                    "required": [ "percent" ]
+                  }
+                }
+              },
+              "required": [ "enabled" ]
+            },
+            "bgpSessionDown": { "$ref": "#/definitions/prometheusAlert" },
+            "extraAlerts": {
+              "type": "array",
+              "items": {
+                "type": "object"
+              }
+            }
+          },
+          "required": [
+            "enabled",
+            "staleConfig",
+            "configNotLoaded",
+            "addressPoolExhausted",
+            "addressPoolUsage",
+            "bgpSessionDown"
+          ]
+        }
+      },
+      "required": [ "podMonitor", "prometheusRule" ]
+    },
+    "speaker": { 
+      "allOf": [
+        { "$ref": "#/definitions/component" },
+        { "description": "MetalLB Speaker",
+          "type": "object",
+          "properties": {
+            "tolerateMaster": {
+              "type": "boolean"
+            },
+            "memberlist": {
+              "type": "object",
+              "properties": {
+                "enabled": {
+                  "type": "boolean"
+                },
+                "mlBindPort": {
+                  "type": "integer"
+                },
+                "mlSecretKeyPath": {
+                  "type": "string"
+                }
+              }
+            },
+            "excludeInterfaces": {
+              "type": "object",
+              "properties": {
+                "enabled": {
+                  "type": "boolean"
+                }
+              }
+            },
+            "updateStrategy": {
+              "type": "object",
+              "properties": {
+                "type": {
+                  "type": "string"
+                }
+              },
+              "required": [ "type" ]
+            },
+            "runtimeClassName": {
+              "type": "string"
+            },
+            "secretName": {
+              "type": "string"
+            },
+            "frr": {
+              "description": "Install FRR container in speaker deployment",
+              "type": "object",
+              "properties": {
+                "enabled": {
+                  "type": "boolean"
+                },
+                "image": { "$ref": "#/definitions/component/properties/image" },
+                "metricsPort": { "type": "integer" },
+                "secureMetricsPort": { "type": "integer" },
+                "resources:": { "type": "object" }
+              },
+              "required": [ "enabled" ]
+            },
+            "command" : {
+              "type": "string"
+            },
+            "reloader": {
+              "type": "object",
+              "properties": {
+                "resources": { "type": "object" }
+              }
+            },
+            "frrMetrics": {
+              "type": "object",
+              "properties": {
+                "resources": { "type": "object" }
+              }
+            }
+          },
+          "required": [ "tolerateMaster" ]
+        }
+      ]
+    },
+    "crds": {
+      "description": "CRD configuration",
+      "type": "object",
+      "properties": {
+        "enabled": {
+          "description": "Enable CRDs",
+          "type": "boolean"
+        },
+        "validationFailurePolicy": {
+          "description": "Failure policy to use with validating webhooks",
+          "type": "string",
+          "enum": [ "Ignore", "Fail" ]
+        }
+      }
+    }
+  },
+  "controller": { 
+    "allOf": [
+      { "$ref": "#/definitions/component" },
+      { "description": "MetalLB Controller",
+        "type": "object",
+        "properties": {
+          "strategy": {
+            "type": "object",
+            "properties": {
+              "type": {
+                "type": "string"
+              }
+            },
+            "required": [ "type" ]
+          },
+          "command" : {
+            "type": "string"
+          },
+          "webhookMode" : {
+            "type": "string"
+          }
+        }
+      }
+    ]
+  },
+  "required": [
+    "controller",
+    "speaker"
+  ]
+}
diff --git a/charts/metallb/values.yaml b/charts/metallb/values.yaml
index 2b990b7..be8cf11 100644
--- a/charts/metallb/values.yaml
+++ b/charts/metallb/values.yaml
@@ -1,12 +1,342 @@
+# Default values for metallb.
+# This is a YAML-formatted file.
+# Declare variables to be passed into your templates.
+
+imagePullSecrets: []
+nameOverride: ""
+fullnameOverride: ""
+loadBalancerClass: ""
+
+# To configure MetalLB, you must specify ONE of the following two
+# options.
+
+rbac:
+  # create specifies whether to install and use RBAC rules.
+  create: true
+
+prometheus:
+  # scrape annotations specifies whether to add Prometheus metric
+  # auto-collection annotations to pods. See
+  # https://github.com/prometheus/prometheus/blob/release-2.1/documentation/examples/prometheus-kubernetes.yml
+  # for a corresponding Prometheus configuration. Alternatively, you
+  # may want to use the Prometheus Operator
+  # (https://github.com/coreos/prometheus-operator) for more powerful
+  # monitoring configuration. If you use the Prometheus operator, this
+  # can be left at false.
+  scrapeAnnotations: false
+
+  # port both controller and speaker will listen on for metrics
+  metricsPort: 7472
+
+  # if set, enables rbac proxy on the controller and speaker to expose
+  # the metrics via tls.
+  # secureMetricsPort: 9120
+
+  # the name of the secret to be mounted in the speaker pod
+  # to expose the metrics securely. If not present, a self signed
+  # certificate to be used.
+  speakerMetricsTLSSecret: ""
+
+  # the name of the secret to be mounted in the controller pod
+  # to expose the metrics securely. If not present, a self signed
+  # certificate to be used.
+  controllerMetricsTLSSecret: ""
+
+  # prometheus doens't have the permission to scrape all namespaces so we give it permission to scrape metallb's one
+  rbacPrometheus: true
+
+  # the service account used by prometheus
+  # required when " .Values.prometheus.rbacPrometheus == true " and " .Values.prometheus.podMonitor.enabled=true or prometheus.serviceMonitor.enabled=true "
+  serviceAccount: ""
+
+  # the namespace where prometheus is deployed
+  # required when " .Values.prometheus.rbacPrometheus == true " and " .Values.prometheus.podMonitor.enabled=true or prometheus.serviceMonitor.enabled=true "
+  namespace: ""
+
+  # the image to be used for the kuberbacproxy container
+  rbacProxy:
+    repository: gcr.io/kubebuilder/kube-rbac-proxy
+    tag: v0.12.0
+    pullPolicy:
+
+  # Prometheus Operator PodMonitors
+  podMonitor:
+    # enable support for Prometheus Operator
+    enabled: false
+
+    # optional additionnal labels for podMonitors
+    additionalLabels: {}
+
+    # optional annotations for podMonitors
+    annotations: {}
+
+    # Job label for scrape target
+    jobLabel: "app.kubernetes.io/name"
+
+    # Scrape interval. If not set, the Prometheus default scrape interval is used.
+    interval:
+
+    # 	metric relabel configs to apply to samples before ingestion.
+    metricRelabelings: []
+    # - action: keep
+    #   regex: 'kube_(daemonset|deployment|pod|namespace|node|statefulset).+'
+    #   sourceLabels: [__name__]
+
+    # 	relabel configs to apply to samples before ingestion.
+    relabelings: []
+    # - sourceLabels: [__meta_kubernetes_pod_node_name]
+    #   separator: ;
+    #   regex: ^(.*)$
+    #   target_label: nodename
+    #   replacement: $1
+    #   action: replace
+
+  # Prometheus Operator ServiceMonitors. To be used as an alternative
+  # to podMonitor, supports secure metrics.
+  serviceMonitor:
+    # enable support for Prometheus Operator
+    enabled: false
+
+    speaker:
+      # optional additional labels for the speaker serviceMonitor
+      additionalLabels: {}
+      # optional additional annotations for the speaker serviceMonitor
+      annotations: {}
+      # optional tls configuration for the speaker serviceMonitor, in case
+      # secure metrics are enabled.
+      tlsConfig:
+        insecureSkipVerify: true
+
+    controller:
+      # optional additional labels for the controller serviceMonitor
+      additionalLabels: {}
+      # optional additional annotations for the controller serviceMonitor
+      annotations: {}
+      # optional tls configuration for the controller serviceMonitor, in case
+      # secure metrics are enabled.
+      tlsConfig:
+        insecureSkipVerify: true
+
+    # Job label for scrape target
+    jobLabel: "app.kubernetes.io/name"
+
+    # Scrape interval. If not set, the Prometheus default scrape interval is used.
+    interval:
+
+    # 	metric relabel configs to apply to samples before ingestion.
+    metricRelabelings: []
+    # - action: keep
+    #   regex: 'kube_(daemonset|deployment|pod|namespace|node|statefulset).+'
+    #   sourceLabels: [__name__]
+
+    # 	relabel configs to apply to samples before ingestion.
+    relabelings: []
+    # - sourceLabels: [__meta_kubernetes_pod_node_name]
+    #   separator: ;
+    #   regex: ^(.*)$
+    #   target_label: nodename
+    #   replacement: $1
+    #   action: replace
+
+  # Prometheus Operator alertmanager alerts
+  prometheusRule:
+    # enable alertmanager alerts
+    enabled: false
+
+    # optional additionnal labels for prometheusRules
+    additionalLabels: {}
+
+    # optional annotations for prometheusRules
+    annotations: {}
+
+    # MetalLBStaleConfig
+    staleConfig:
+      enabled: true
+      labels:
+        severity: warning
+
+    # MetalLBConfigNotLoaded
+    configNotLoaded:
+      enabled: true
+      labels:
+        severity: warning
+
+    # MetalLBAddressPoolExhausted
+    addressPoolExhausted:
+      enabled: true
+      labels:
+        severity: alert
+
+    addressPoolUsage:
+      enabled: true
+      thresholds:
+        - percent: 75
+          labels:
+            severity: warning
+        - percent: 85
+          labels:
+            severity: warning
+        - percent: 95
+          labels:
+            severity: alert
+
+    # MetalLBBGPSessionDown
+    bgpSessionDown:
+      enabled: true
+      labels:
+        severity: alert
+
+    extraAlerts: []
+
+# controller contains configuration specific to the MetalLB cluster
+# controller.
 controller:
+  enabled: true
+  # -- Controller log level. Must be one of: `all`, `debug`, `info`, `warn`, `error` or `none`
+  logLevel: info
+  # command: /controller
+  # webhookMode: enabled
   image:
     repository: quay.io/metallb/controller
     tag:
     pullPolicy:
-  logLevel: info
+  ## @param controller.updateStrategy.type Metallb controller deployment strategy type.
+  ## ref: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#strategy
+  ## e.g:
+  ## strategy:
+  ##  type: RollingUpdate
+  ##  rollingUpdate:
+  ##    maxSurge: 25%
+  ##    maxUnavailable: 25%
+  ##
+  strategy:
+    type: RollingUpdate
+  serviceAccount:
+    # Specifies whether a ServiceAccount should be created
+    create: true
+    # The name of the ServiceAccount to use. If not set and create is
+    # true, a name is generated using the fullname template
+    name: ""
+    annotations: {}
+  securityContext:
+    runAsNonRoot: true
+    # nobody
+    runAsUser: 65534
+    fsGroup: 65534
+  resources: {}
+    # limits:
+      # cpu: 100m
+      # memory: 100Mi
+  nodeSelector: {}
+  tolerations: []
+  priorityClassName: ""
+  runtimeClassName: ""
+  affinity: {}
+  podAnnotations: {}
+  labels: {}
+  livenessProbe:
+    enabled: true
+    failureThreshold: 3
+    initialDelaySeconds: 10
+    periodSeconds: 10
+    successThreshold: 1
+    timeoutSeconds: 1
+  readinessProbe:
+    enabled: true
+    failureThreshold: 3
+    initialDelaySeconds: 10
+    periodSeconds: 10
+    successThreshold: 1
+    timeoutSeconds: 1
+
+# speaker contains configuration specific to the MetalLB speaker
+# daemonset.
 speaker:
+  enabled: true
+  # command: /speaker
+  # -- Speaker log level. Must be one of: `all`, `debug`, `info`, `warn`, `error` or `none`
+  logLevel: info
+  tolerateMaster: true
+  memberlist:
+    enabled: true
+    mlBindPort: 7946
+    mlSecretKeyPath: "/etc/ml_secret_key"
+  excludeInterfaces:
+    enabled: true
   image:
     repository: quay.io/metallb/speaker
     tag:
     pullPolicy:
-  logLevel: info
+  ## @param speaker.updateStrategy.type Speaker daemonset strategy type
+  ## ref: https://kubernetes.io/docs/tasks/manage-daemon/update-daemon-set/
+  ##
+  updateStrategy:
+    ## StrategyType
+    ## Can be set to RollingUpdate or OnDelete
+    ##
+    type: RollingUpdate
+  serviceAccount:
+    # Specifies whether a ServiceAccount should be created
+    create: true
+    # The name of the ServiceAccount to use. If not set and create is
+    # true, a name is generated using the fullname template
+    name: ""
+    annotations: {}
+  ## Defines a secret name for the controller to generate a memberlist encryption secret
+  ## By default secretName: {{ "metallb.fullname" }}-memberlist
+  ##
+  # secretName:
+  resources: {}
+    # limits:
+      # cpu: 100m
+      # memory: 100Mi
+  nodeSelector: {}
+  tolerations: []
+  priorityClassName: ""
+  affinity: {}
+  ## Selects which runtime class will be used by the pod.
+  runtimeClassName: ""
+  podAnnotations: {}
+  labels: {}
+  livenessProbe:
+    enabled: true
+    failureThreshold: 3
+    initialDelaySeconds: 10
+    periodSeconds: 10
+    successThreshold: 1
+    timeoutSeconds: 1
+  readinessProbe:
+    enabled: true
+    failureThreshold: 3
+    initialDelaySeconds: 10
+    periodSeconds: 10
+    successThreshold: 1
+    timeoutSeconds: 1
+  startupProbe:
+    enabled: true
+    failureThreshold: 30
+    periodSeconds: 5
+  # frr contains configuration specific to the MetalLB FRR container,
+  # for speaker running alongside FRR.
+  frr:
+    enabled: true
+    image:
+      repository: quay.io/frrouting/frr
+      tag: 8.5.2
+      pullPolicy:
+    metricsPort: 7473
+    resources: {}
+
+    # if set, enables a rbac proxy sidecar container on the speaker to
+    # expose the frr metrics via tls.
+    # secureMetricsPort: 9121
+
+  reloader:
+    resources: {}
+
+  frrMetrics:
+    resources: {}
+
+crds:
+  enabled: true
+  validationFailurePolicy: Fail