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()