Merge "Don't search for nested repositories during git-gc"
diff --git a/Documentation/operator-api-reference.md b/Documentation/operator-api-reference.md
index 5b05bf4..7989a8c 100644
--- a/Documentation/operator-api-reference.md
+++ b/Documentation/operator-api-reference.md
@@ -7,38 +7,48 @@
    3. [Gerrit](#gerrit)
    4. [Receiver](#receiver)
    5. [GitGarbageCollection](#gitgarbagecollection)
-   6. [GerritClusterSpec](#gerritclusterspec)
-   7. [GerritClusterStatus](#gerritclusterstatus)
-   8. [GerritStorageConfig](#gerritstorageconfig)
-   9. [StorageClassConfig](#storageclassconfig)
-   10. [NfsWorkaroundConfig](#nfsworkaroundconfig)
-   11. [SharedStorage](#sharedstorage)
-   12. [OptionalSharedStorage](#optionalsharedstorage)
-   13. [ContainerImageConfig](#containerimageconfig)
-   14. [BusyBoxImage](#busyboximage)
-   15. [GerritRepositoryConfig](#gerritrepositoryconfig)
-   16. [GerritClusterIngressConfig](#gerritclusteringressconfig)
-   17. [IngressType](#ingresstype)
-   18. [GerritIngressTlsConfig](#gerritingresstlsconfig)
-   19. [GerritTemplate](#gerrittemplate)
-   20. [GerritTemplateSpec](#gerrittemplatespec)
-   21. [GerritProbe](#gerritprobe)
-   22. [GerritServiceConfig](#gerritserviceconfig)
-   23. [GerritSite](#gerritsite)
-   24. [GerritPlugin](#gerritplugin)
-   25. [GerritMode](#gerritmode)
-   26. [GerritSpec](#gerritspec)
-   27. [GerritStatus](#gerritstatus)
-   28. [IngressConfig](#ingressconfig)
-   29. [ReceiverTemplate](#receivertemplate)
-   30. [ReceiverTemplateSpec](#receivertemplatespec)
-   31. [ReceiverSpec](#receiverspec)
-   32. [ReceiverStatus](#receiverstatus)
-   33. [ReceiverProbe](#receiverprobe)
-   34. [ReceiverServiceConfig](#receiverserviceconfig)
-   35. [GitGarbageCollectionSpec](#gitgarbagecollectionspec)
-   36. [GitGarbageCollectionStatus](#gitgarbagecollectionstatus)
-   37. [GitGcState](#gitgcstate)
+   6. [GerritNetwork](#gerritnetwork)
+   7. [GerritClusterSpec](#gerritclusterspec)
+   8. [GerritClusterStatus](#gerritclusterstatus)
+   9. [StorageConfig](#storageconfig)
+   10. [GerritStorageConfig](#gerritstorageconfig)
+   11. [StorageClassConfig](#storageclassconfig)
+   12. [NfsWorkaroundConfig](#nfsworkaroundconfig)
+   13. [SharedStorage](#sharedstorage)
+   14. [PluginCacheConfig](#plugincacheconfig)
+   15. [ExternalPVCConfig](#externalpvcconfig)
+   16. [ContainerImageConfig](#containerimageconfig)
+   17. [BusyBoxImage](#busyboximage)
+   18. [GerritRepositoryConfig](#gerritrepositoryconfig)
+   19. [GerritClusterIngressConfig](#gerritclusteringressconfig)
+   20. [GerritIngressTlsConfig](#gerritingresstlsconfig)
+   21. [GlobalRefDbConfig](#globalrefdbconfig)
+   22. [RefDatabase](#refdatabase)
+   23. [ZookeeperRefDbConfig](#zookeeperrefdbconfig)
+   24. [GerritTemplate](#gerrittemplate)
+   25. [GerritTemplateSpec](#gerrittemplatespec)
+   26. [GerritProbe](#gerritprobe)
+   27. [GerritServiceConfig](#gerritserviceconfig)
+   28. [GerritSite](#gerritsite)
+   29. [GerritModule](#gerritmodule)
+   30. [GerritPlugin](#gerritplugin)
+   31. [GerritMode](#gerritmode)
+   32. [GerritDebugConfig](#gerritdebugconfig)
+   33. [GerritSpec](#gerritspec)
+   34. [GerritStatus](#gerritstatus)
+   35. [IngressConfig](#ingressconfig)
+   36. [ReceiverTemplate](#receivertemplate)
+   37. [ReceiverTemplateSpec](#receivertemplatespec)
+   38. [ReceiverSpec](#receiverspec)
+   39. [ReceiverStatus](#receiverstatus)
+   40. [ReceiverProbe](#receiverprobe)
+   41. [ReceiverServiceConfig](#receiverserviceconfig)
+   42. [GitGarbageCollectionSpec](#gitgarbagecollectionspec)
+   43. [GitGarbageCollectionStatus](#gitgarbagecollectionstatus)
+   44. [GitGcState](#gitgcstate)
+   45. [GerritNetworkSpec](#gerritnetworkspec)
+   46. [NetworkMember](#networkmember)
+   47. [NetworkMemberWithSsh](#networkmemberwithssh)
 
 ## General Remarks
 
@@ -53,7 +63,7 @@
 ---
 
 **Group**: gerritoperator.google.com \
-**Version**: v1alpha3 \
+**Version**: v1alpha15 \
 **Kind**: GerritCluster
 
 ---
@@ -70,7 +80,7 @@
 Example:
 
 ```yaml
-apiVersion: "gerritoperator.google.com/v1alpha3"
+apiVersion: "gerritoperator.google.com/v1alpha15"
 kind: GerritCluster
 metadata:
   name: gerrit
@@ -102,7 +112,10 @@
             Nobody-User = nobody
             Nobody-Group = nogroup
 
-    gitRepositoryStorage:
+    sharedStorage:
+      externalPVC:
+        enabled: false
+        claimName: ""
       size: 1Gi
       volumeName: ""
       selector:
@@ -110,38 +123,33 @@
           volume-type: ssd
           aws-availability-zone: us-east-1
 
-    logsStorage:
-      size: 1Gi
-      volumeName: ""
-      selector:
-        matchLabels:
-          volume-type: ssd
-          aws-availability-zone: us-east-1
-
-    pluginCacheStorage:
+    pluginCache:
       enabled: false
-      size: 1Gi
-      volumeName: ""
-      selector:
-        matchLabels:
-          volume-type: ssd
-          aws-availability-zone: us-east-1
 
   ingress:
     enabled: true
-    type: INGRESS
     host: example.com
     annotations: {}
     tls:
       enabled: false
       secret: ""
 
+  refdb:
+    database: NONE
+    zookeeper:
+      connectString: ""
+      rootNode: ""
+
+  serverId: ""
+
   gerrits:
   - metadata:
       name: gerrit
       labels:
         app: gerrit
     spec:
+      serviceAccount: gerrit
+
       tolerations:
       - key: key1
         operator: Equal
@@ -209,6 +217,10 @@
 
       mode: REPLICA
 
+      debug:
+        enabled: false
+        suspend: false
+
       site:
         size: 1Gi
 
@@ -228,6 +240,11 @@
         sha1: 6dfe8292d46b179638586e6acf671206f4e0a88b
         installAsLibrary: true
 
+      libs:
+      - name: global-refdb
+        url: https://example.com/global-refdb.jar
+        sha1: 3d533a536b0d4e0184f824478c24bc8dfe896d06
+
       configFiles:
         gerrit.config: |-
             [gerrit]
@@ -322,7 +339,7 @@
 ---
 
 **Group**: gerritoperator.google.com \
-**Version**: v1alpha4 \
+**Version**: v1alpha16 \
 **Kind**: Gerrit
 
 ---
@@ -339,19 +356,21 @@
 Example:
 
 ```yaml
-apiVersion: "gerritoperator.google.com/v1alpha4"
+apiVersion: "gerritoperator.google.com/v1alpha16"
 kind: Gerrit
 metadata:
   name: gerrit
 spec:
+  serviceAccount: gerrit
+
   tolerations:
     - key: key1
       operator: Equal
       value: value1
       effect: NoSchedule
 
-    affinity:
-      nodeAffinity:
+  affinity:
+    nodeAffinity:
       requiredDuringSchedulingIgnoredDuringExecution:
         nodeSelectorTerms:
         - matchExpressions:
@@ -360,99 +379,110 @@
             values:
             - ssd
 
-    topologySpreadConstraints: []
-    - maxSkew: 1
-      topologyKey: zone
-      whenUnsatisfiable: DoNotSchedule
-      labelSelector:
-        matchLabels:
-          foo: bar
+  topologySpreadConstraints:
+  - maxSkew: 1
+    topologyKey: zone
+    whenUnsatisfiable: DoNotSchedule
+    labelSelector:
+      matchLabels:
+        foo: bar
 
-    priorityClassName: ""
+  priorityClassName: ""
 
-    replicas: 1
-    updatePartition: 0
+  replicas: 1
+  updatePartition: 0
 
-    resources:
-      requests:
-        cpu: 1
-        memory: 5Gi
-      limits:
-        cpu: 1
-        memory: 6Gi
+  resources:
+    requests:
+      cpu: 1
+      memory: 5Gi
+    limits:
+      cpu: 1
+      memory: 6Gi
 
-    startupProbe:
-      initialDelaySeconds: 0
-      periodSeconds: 10
-      timeoutSeconds: 1
-      successThreshold: 1
-      failureThreshold: 3
+  startupProbe:
+    initialDelaySeconds: 0
+    periodSeconds: 10
+    timeoutSeconds: 1
+    successThreshold: 1
+    failureThreshold: 3
 
-    readinessProbe:
-      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
+  livenessProbe:
+    initialDelaySeconds: 0
+    periodSeconds: 10
+    timeoutSeconds: 1
+    successThreshold: 1
+    failureThreshold: 3
 
-    gracefulStopTimeout: 30
+  gracefulStopTimeout: 30
 
-    service:
-      type: NodePort
-      httpPort: 80
-      sshPort: 29418
+  service:
+    type: NodePort
+    httpPort: 80
+    sshPort: 29418
 
-    mode: PRIMARY
+  mode: PRIMARY
 
-    site:
-      size: 1Gi
+  debug:
+    enabled: false
+    suspend: false
 
-    plugins:
-    # Installs a plugin packaged into the gerrit.war file
-    - name: delete-project
+  site:
+    size: 1Gi
 
-    # 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
+  plugins:
+  # Installs a plugin packaged into the gerrit.war file
+  - name: delete-project
 
-    # 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
+  # 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
 
-    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
+  # 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
 
-    secretRef: gerrit-secure-config
+  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: []
@@ -481,7 +511,10 @@
             Nobody-User = nobody
             Nobody-Group = nogroup
 
-    gitRepositoryStorage:
+    sharedStorage:
+      externalPVC:
+        enabled: false
+        claimName: ""
       size: 1Gi
       volumeName: ""
       selector:
@@ -489,27 +522,18 @@
           volume-type: ssd
           aws-availability-zone: us-east-1
 
-    logsStorage:
-      size: 1Gi
-      volumeName: ""
-      selector:
-        matchLabels:
-          volume-type: ssd
-          aws-availability-zone: us-east-1
-
-    pluginCacheStorage:
+    pluginCache:
       enabled: false
-      size: 1Gi
-      volumeName: ""
-      selector:
-        matchLabels:
-          volume-type: ssd
-          aws-availability-zone: us-east-1
 
   ingress:
-    type: INGRESS
     host: example.com
     tlsEnabled: false
+
+  refdb:
+    database: NONE
+    zookeeper:
+      connectString: ""
+      rootNode: ""
 ```
 
 ## Receiver
@@ -517,7 +541,7 @@
 ---
 
 **Group**: gerritoperator.google.com \
-**Version**: v1alpha2 \
+**Version**: v1alpha6 \
 **Kind**: Receiver
 
 ---
@@ -534,12 +558,11 @@
 Example:
 
 ```yaml
-apiVersion: "gerritoperator.google.com/v1alpha2"
+apiVersion: "gerritoperator.google.com/v1alpha6"
 kind: Receiver
 metadata:
   name: receiver
 spec:
-  cluster: gerrit
   tolerations:
   - key: key1
     operator: Equal
@@ -570,7 +593,7 @@
   maxSurge: 1
   maxUnavailable: 1
 
-  resources: {}
+  resources:
     requests:
       cpu: 1
       memory: 5Gi
@@ -625,15 +648,10 @@
             Nobody-User = nobody
             Nobody-Group = nogroup
 
-    gitRepositoryStorage:
-      size: 1Gi
-      volumeName: ""
-      selector:
-        matchLabels:
-          volume-type: ssd
-          aws-availability-zone: us-east-1
-
-    logsStorage:
+    sharedStorage:
+      externalPVC:
+        enabled: false
+        claimName: ""
       size: 1Gi
       volumeName: ""
       selector:
@@ -642,7 +660,6 @@
           aws-availability-zone: us-east-1
 
   ingress:
-    type: INGRESS
     host: example.com
     tlsEnabled: false
 ```
@@ -704,6 +721,52 @@
             - ssd
 ```
 
+## GerritNetwork
+
+---
+
+**Group**: gerritoperator.google.com \
+**Version**: v1alpha1 \
+**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/v1alpha1"
+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 |
@@ -711,6 +774,8 @@
 | `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. |
 
@@ -720,14 +785,20 @@
 |---|---|---|
 | `members` | `Map<String, List<String>>` | A map listing all Gerrit and Receiver instances managed by the GerritCluster by name |
 
-## GerritStorageConfig
+## StorageConfig
 
 | Field | Type | Description |
 |---|---|---|
 | `storageClasses` | [`StorageClassConfig`](#storageclassconfig) | StorageClasses used in the GerritCluster |
-| `gitRepositoryStorage` | [`SharedStorage`](#sharedstorage) | Volume used for storing Git repositories |
-| `logsStorage` | [`SharedStorage`](#sharedstorage) | Volume used for storing logs |
-| `pluginCacheStorage` | [`OptionalSharedStorage`](#optionalsharedstorage) | Volume used for caching downloaded plugin JAR-files (Only used by Gerrit resources. Otherwise ignored.) |
+| `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
 
@@ -749,17 +820,23 @@
 
 | 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) |
 
-## OptionalSharedStorage
-
-**Extends:** [`SharedStorage`](#sharedstorage)
+## PluginCacheConfig
 
 | Field | Type | Description |
 |---|---|---|
-| `enabled` | `boolean` | Whether to enable this storage. (default: `false`) |
+| `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
 
@@ -790,19 +867,10 @@
 | Field | Type | Description |
 |---|---|---|
 | `enabled` | `boolean` | Whether to configure an ingress provider to manage the ingress traffic in the GerritCluster (default: `false`) |
-| `type` | [`IngressType`](#ingresstype) | Which type of ingress provider to use (default: `NONE`) |
 | `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 |
 
-## IngressType
-
-| Value | Description|
-|---|---|
-| `NONE` | No ingress provider will be configured |
-| `INGRESS` | An [`Ingress`](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#ingress-v1-networking-k8s-io) will be provisioned. Only the [Nginx-Ingress-Controller](https://docs.nginx.com/nginx-ingress-controller/) is supported. |
-| `ISTIO` | [ISTIO](https://istio.io/latest/) will be configured to add the GerritCluster to the ServiceMesh |
-
 ## GerritIngressTlsConfig
 
 | Field | Type | Description |
@@ -810,6 +878,31 @@
 | `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. |
 
+## 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`, `ZOOKEEPER`. (default: `NONE`) |
+| `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. |
+| `ZOOKEEPER` | Zookeeper will be used as a global refdb |
+
+## 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 |
@@ -821,6 +914,7 @@
 
 | 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) |
@@ -835,9 +929,11 @@
 | `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
 
@@ -860,13 +956,20 @@
 |---|---|---|
 | `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) |
 
-## GerritPlugin
+## GerritModule
 
 | Field | Type | Description |
 |---|---|---|
-| `name` | `String` | Name of the plugin |
-| `url` | `URL` | URL of the plugin, if it should be downloaded. If the URL is not set, the plugin is expected to be packaged in the war-file (optional) |
-| `sha1` | `String` | SHA1-checksum of the plugin JAR-file. (mandatory, if `url` is set) |
+| `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
@@ -876,6 +979,20 @@
 | `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)
@@ -885,6 +1002,8 @@
 | `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
 
@@ -898,7 +1017,6 @@
 
 | Field | Type | Description |
 |---|---|---|
-| `type` | [`IngressType`](#ingresstype) | Which type of ingress provider is being used. |
 | `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`) |
 
@@ -932,7 +1050,7 @@
 
 | Field | Type | Description |
 |---|---|---|
-| `storage` | [`GerritStorageConfig`](#gerritstorageconfig) | Storage used by Gerrit/Receiver instances |
+| `storage` | [`StorageConfig`](#storageconfig) | Storage used by Gerrit/Receiver instances |
 | `containerImages` | [`ContainerImageConfig`](#containerimageconfig) | Container images used inside GerritCluster |
 | `ingress` | [`IngressConfig`](#ingressconfig) | Ingress configuration for Gerrit |
 
@@ -984,3 +1102,27 @@
 | `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/Documentation/operator.md b/Documentation/operator.md
index 61b2cf7..e6acf21 100644
--- a/Documentation/operator.md
+++ b/Documentation/operator.md
@@ -18,6 +18,7 @@
       2. [Gerrit](#gerrit)
       3. [GitGarbageCollection](#gitgarbagecollection)
       4. [Receiver](#receiver)
+      5. [GerritNetwork](#gerritnetwork)
    8. [Configuration of Gerrit](#configuration-of-gerrit)
 
 ## Build
@@ -159,6 +160,10 @@
   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
@@ -167,6 +172,9 @@
   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.
@@ -259,11 +267,14 @@
 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.
+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` or `ISTIO`.
 
 ## CustomResources
 
@@ -340,6 +351,14 @@
 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
diff --git a/Pipfile b/Pipfile
index eb26474..18ace64 100644
--- a/Pipfile
+++ b/Pipfile
@@ -4,23 +4,23 @@
 verify_ssl = true
 
 [dev-packages]
-pylint = "~=2.12.2"
-black = "~=22.3.0"
+pylint = "~=2.17.5"
+black = "~=23.7.0"
 
 [packages]
-docker = "~=6.0.1"
-pytest = "~=6.2.5"
+docker = "~=6.1.3"
+pytest = "~=7.4.0"
 passlib = "~=1.7.4"
-pyopenssl = "~=22.1.0"
+pyopenssl = "~=23.2.0"
 requests = "~=2.31.0"
 pytest-timeout = "~=2.1.0"
-kubernetes = "~=21.7.0"
-pygit2 = "~=1.9.1"
-selenium = "~=4.2.0"
-chromedriver-autoinstaller = "==0.3.1"
+kubernetes = "~=27.2.0"
+pygit2 = "~=1.12.2"
+selenium = "~=4.11.2"
+chromedriver-autoinstaller = "==0.6.2"
 
 [requires]
-python_version = "3.9"
+python_version = "3.11"
 
 [pipenv]
 allow_prereleases = true
diff --git a/Pipfile.lock b/Pipfile.lock
index 29e4385..9678c9a 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,11 +1,11 @@
 {
     "_meta": {
         "hash": {
-            "sha256": "de76c77607ff03042a993bcc9b27e9bb84025f5d17a160b45b3df934b5b6ffc1"
+            "sha256": "db93e37abb75873f53120e5f4871bead84d2c21f587da243a9d7729d4ed00a55"
         },
         "pipfile-spec": 6,
         "requires": {
-            "python_version": "3.9"
+            "python_version": "3.11"
         },
         "sources": [
             {
@@ -16,14 +16,6 @@
         ]
     },
     "default": {
-        "async-generator": {
-            "hashes": [
-                "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b",
-                "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144"
-            ],
-            "markers": "python_version >= '3.5'",
-            "version": "==1.10"
-        },
         "attrs": {
             "hashes": [
                 "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04",
@@ -34,19 +26,19 @@
         },
         "cachetools": {
             "hashes": [
-                "sha256:13dfddc7b8df938c21a940dfa6557ce6e94a2f1cdfa58eb90c805721d58f2c14",
-                "sha256:429e1a1e845c008ea6c85aa35d4b98b65d6a9763eeef3e37e92728a12d1de9d4"
+                "sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590",
+                "sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b"
             ],
-            "markers": "python_version ~= '3.7'",
-            "version": "==5.3.0"
+            "markers": "python_version >= '3.7'",
+            "version": "==5.3.1"
         },
         "certifi": {
             "hashes": [
-                "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7",
-                "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"
+                "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082",
+                "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"
             ],
             "markers": "python_version >= '3.6'",
-            "version": "==2023.5.7"
+            "version": "==2023.7.22"
         },
         "cffi": {
             "hashes": [
@@ -119,148 +111,145 @@
         },
         "charset-normalizer": {
             "hashes": [
-                "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6",
-                "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1",
-                "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e",
-                "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373",
-                "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62",
-                "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230",
-                "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be",
-                "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c",
-                "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0",
-                "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448",
-                "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f",
-                "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649",
-                "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d",
-                "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0",
-                "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706",
-                "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a",
-                "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59",
-                "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23",
-                "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5",
-                "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb",
-                "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e",
-                "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e",
-                "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c",
-                "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28",
-                "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d",
-                "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41",
-                "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974",
-                "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce",
-                "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f",
-                "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1",
-                "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d",
-                "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8",
-                "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017",
-                "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31",
-                "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7",
-                "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8",
-                "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e",
-                "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14",
-                "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd",
-                "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d",
-                "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795",
-                "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b",
-                "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b",
-                "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b",
-                "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203",
-                "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f",
-                "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19",
-                "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1",
-                "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a",
-                "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac",
-                "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9",
-                "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0",
-                "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137",
-                "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f",
-                "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6",
-                "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5",
-                "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909",
-                "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f",
-                "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0",
-                "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324",
-                "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755",
-                "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb",
-                "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854",
-                "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c",
-                "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60",
-                "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84",
-                "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0",
-                "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b",
-                "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1",
-                "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531",
-                "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1",
-                "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11",
-                "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326",
-                "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df",
-                "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"
+                "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.1.0"
+            "version": "==3.2.0"
         },
         "chromedriver-autoinstaller": {
             "hashes": [
-                "sha256:7bafc2c1730fc044d078a9e2e31c59c8015077c7e4de3903f7f14693af03bfbe",
-                "sha256:d2d934fed5a8c27352e279c8c398ecbc570f5aebb4a877add05dc4703ce91bc8"
+                "sha256:7055e3e5a64e4352855fafab15d266e2ed325620222224fb261a2131e821dfe3",
+                "sha256:8ff5c715160b294c9e7cc0fae5ecc5ccaff5563ca1405daed6b959cca606e57c"
             ],
             "index": "pypi",
-            "version": "==0.3.1"
+            "version": "==0.6.2"
         },
         "cryptography": {
             "hashes": [
-                "sha256:0e70da4bdff7601b0ef48e6348339e490ebfb0cbe638e083c9c41fb49f00c8bd",
-                "sha256:10652dd7282de17990b88679cb82f832752c4e8237f0c714be518044269415db",
-                "sha256:175c1a818b87c9ac80bb7377f5520b7f31b3ef2a0004e2420319beadedb67290",
-                "sha256:1d7e632804a248103b60b16fb145e8df0bc60eed790ece0d12efe8cd3f3e7744",
-                "sha256:1f13ddda26a04c06eb57119caf27a524ccae20533729f4b1e4a69b54e07035eb",
-                "sha256:2ec2a8714dd005949d4019195d72abed84198d877112abb5a27740e217e0ea8d",
-                "sha256:2fa36a7b2cc0998a3a4d5af26ccb6273f3df133d61da2ba13b3286261e7efb70",
-                "sha256:2fb481682873035600b5502f0015b664abc26466153fab5c6bc92c1ea69d478b",
-                "sha256:3178d46f363d4549b9a76264f41c6948752183b3f587666aff0555ac50fd7876",
-                "sha256:4367da5705922cf7070462e964f66e4ac24162e22ab0a2e9d31f1b270dd78083",
-                "sha256:4eb85075437f0b1fd8cd66c688469a0c4119e0ba855e3fef86691971b887caf6",
-                "sha256:50a1494ed0c3f5b4d07650a68cd6ca62efe8b596ce743a5c94403e6f11bf06c1",
-                "sha256:53049f3379ef05182864d13bb9686657659407148f901f3f1eee57a733fb4b00",
-                "sha256:6391e59ebe7c62d9902c24a4d8bcbc79a68e7c4ab65863536127c8a9cd94043b",
-                "sha256:67461b5ebca2e4c2ab991733f8ab637a7265bb582f07c7c88914b5afb88cb95b",
-                "sha256:78e47e28ddc4ace41dd38c42e6feecfdadf9c3be2af389abbfeef1ff06822285",
-                "sha256:80ca53981ceeb3241998443c4964a387771588c4e4a5d92735a493af868294f9",
-                "sha256:8a4b2bdb68a447fadebfd7d24855758fe2d6fecc7fed0b78d190b1af39a8e3b0",
-                "sha256:8e45653fb97eb2f20b8c96f9cd2b3a0654d742b47d638cf2897afbd97f80fa6d",
-                "sha256:998cd19189d8a747b226d24c0207fdaa1e6658a1d3f2494541cb9dfbf7dcb6d2",
-                "sha256:a10498349d4c8eab7357a8f9aa3463791292845b79597ad1b98a543686fb1ec8",
-                "sha256:b4cad0cea995af760f82820ab4ca54e5471fc782f70a007f31531957f43e9dee",
-                "sha256:bfe6472507986613dc6cc00b3d492b2f7564b02b3b3682d25ca7f40fa3fd321b",
-                "sha256:c9e0d79ee4c56d841bd4ac6e7697c8ff3c8d6da67379057f29e66acffcd1e9a7",
-                "sha256:ca57eb3ddaccd1112c18fc80abe41db443cc2e9dcb1917078e02dfa010a4f353",
-                "sha256:ce127dd0a6a0811c251a6cddd014d292728484e530d80e872ad9806cfb1c5b3c"
+                "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.6'",
-            "version": "==38.0.4"
+            "markers": "python_version >= '3.7'",
+            "version": "==41.0.3"
         },
         "docker": {
             "hashes": [
-                "sha256:896c4282e5c7af5c45e8b683b0b0c33932974fe6e50fc6906a0a83616ab3da97",
-                "sha256:dbcb3bd2fa80dca0788ed908218bf43972772009b881ed1e20dfc29a65e49782"
+                "sha256:aa6d17830045ba5ef0168d5eaa34d37beeb113948c413affe1d5991fc11f9a20",
+                "sha256:aecd2277b8bf8e506e484f6ab7aec39abe0038e29fa4a6d3ba86c3fe01844ed9"
             ],
             "index": "pypi",
-            "version": "==6.0.1"
+            "version": "==6.1.3"
         },
         "exceptiongroup": {
             "hashes": [
-                "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e",
-                "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785"
+                "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9",
+                "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"
             ],
             "markers": "python_version < '3.11'",
-            "version": "==1.1.1"
+            "version": "==1.1.3"
         },
         "google-auth": {
             "hashes": [
-                "sha256:55a395cdfd3f3dd3f649131d41f97c17b4ed8a2aac1be3502090c716314e8a37",
-                "sha256:d7a3249027e7f464fbbfd7ee8319a08ad09d2eea51578575c4bd360ffa049ccb"
+                "sha256:164cba9af4e6e4e40c3a4f90a1a6c12ee56f14c0b4868d1ca91b32826ab334ce",
+                "sha256:d61d1b40897407b574da67da1a833bdc10d5a11642566e506565d1b1a46ba873"
             ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
-            "version": "==2.18.1"
+            "markers": "python_version >= '3.6'",
+            "version": "==2.22.0"
         },
         "h11": {
             "hashes": [
@@ -288,11 +277,11 @@
         },
         "kubernetes": {
             "hashes": [
-                "sha256:044c20253f8577491a87af8f9edea1f929ed6d62ce306376a6cb8aed24e572c5",
-                "sha256:c9849afc2eafdce60efa210049ee7a94e7ef6cf3a7afa14a69b3bf0447825977"
+                "sha256:0f9376329c85cf07615ed6886bf9bf21eb1cbfc05e14ec7b0f74ed8153cd2815",
+                "sha256:d479931c6f37561dbfdf28fc5f46384b1cb8b28f9db344ed4a232ce91990825a"
             ],
             "index": "pypi",
-            "version": "==21.7.0"
+            "version": "==27.2.0"
         },
         "oauthlib": {
             "hashes": [
@@ -328,19 +317,11 @@
         },
         "pluggy": {
             "hashes": [
-                "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159",
-                "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"
+                "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849",
+                "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"
             ],
-            "markers": "python_version >= '3.6'",
-            "version": "==1.0.0"
-        },
-        "py": {
-            "hashes": [
-                "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719",
-                "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
-            "version": "==1.11.0"
+            "markers": "python_version >= '3.7'",
+            "version": "==1.2.0"
         },
         "pyasn1": {
             "hashes": [
@@ -367,50 +348,48 @@
         },
         "pygit2": {
             "hashes": [
-                "sha256:084bc622205b6f495a0c7e6d8dde9a2e42967bd6b8e16e28d21725dbcc837e1a",
-                "sha256:0a0aaadca823c2e6d1f6319190f53c55c8323a810a1d1117e378e907c98cf613",
-                "sha256:14b51a909debdfdaa7757a581a1c6f6d1a5b150870da68881d3bd9d5b94842c7",
-                "sha256:20894433df1146481aacae37e2b0f3bbbfdea026db2f55061170bd9823e40b19",
-                "sha256:3068375e81a473d01d23d86abc5e978bac7bd277a91538416d31e06d0e97402f",
-                "sha256:308ce00e8a1f8d8dc3858b3e21f0ea701cdde675966aea68fcccf559cb5e9577",
-                "sha256:3f737e8eb42a818de2e604bfca125e79e3f386e8b77cceb1fe881f7603c378c2",
-                "sha256:4625e8957b9e7e72a300d42e27e5392ac449517397fb22045b8c3e468f4b6f06",
-                "sha256:487ae81134b44b1e0173b3e9a478f93f18c1c22d53241d1fc8047e400094582b",
-                "sha256:490d6ba5ae4a539d147644e9ce20a2c5dd55dd3ea177cec78971b7422c0540d4",
-                "sha256:55593d734a30e824f9136e0afcd15b287125ef41dac7833f564da454ba0969d0",
-                "sha256:5f038afaeaf5cd1fa35ae02073f42558eb7daf6cbc57cdc41e5ee9dfdad6a653",
-                "sha256:6724885e4a31a843fd6d7d6cd90baef5c61a774d222fabbe39505c0b3dd2c55b",
-                "sha256:6a1ebd104105cc56ae2ba100090228a4db8cbeb7a480e8657a803d674331b82d",
-                "sha256:6e7f56bf5338ec79e7521204ddf4f6848cd2ccd1de4ea8b2c0af163ed4b08ade",
-                "sha256:75a95ddab5d256c35377a2892bd5f5f3121552c3ae9af9b06eaa7ac426220d22",
-                "sha256:7cef5b08544b895a75ed7908ca6c0d730b890ffeba7f2b46e5c8aec458786802",
-                "sha256:81e1d25b2a0be1a8cd7d4131fe5af8efddc7015f522638e2c53fe820800e4de6",
-                "sha256:871682c3a910d71cc8bf6f8be4474085bf3eb27864a090f2132f6fa50fe2eb30",
-                "sha256:8a5bf52fb75dc2d2da814996b8006559d0b57b573775f757a1997f89eabfdb0a",
-                "sha256:8b6d7b3613ef8358f24d32e4a1ef976218e351e84953c474d1fa1d29b28484db",
-                "sha256:8cdf963725b1f6bfad12a9238a421587af682164d90b3d5a81224d4a112ed4f6",
-                "sha256:9bfd9d089942482ca0b5f426396b76fb86b25ca3414546388d8cfa8824ab1188",
-                "sha256:bf160b3653168e5d11e6de9589018db55ef51a0859bf4a3719aa8cc0998c584e",
-                "sha256:c0c18ed4ce7f06e6885ab01f1b1f80468e09d1bd72265e14575be0b44a581ae7",
-                "sha256:c6198c1010af273d91c182997693d61b2d00edeecdef9c39beef711568bec984",
-                "sha256:d2cb8571cd02acf739b26d2c2bb4828f7cfb4e23b564d6c4442bffe8714ec8e5",
-                "sha256:d52113184c38455bbc9576003054311d8c283a547a12790baf0210ccfd0cc90f",
-                "sha256:da12d67bdb43736e3bd6464623e8aff06796527ea8525f65b76a776f26c7fa24",
-                "sha256:eb2d916ec03c1dda7ab04506d42ef2c0bac2590827c5d15fec49b67f39f02704",
-                "sha256:ebe0b2371fe4d91adc5014cc94dc85497bec6a5e1e557856bb45f586e31519bd",
-                "sha256:f84826586d5e7f32e560d0d55fd35484cebd49fefccfe8a3727bd4b7c4788b92",
-                "sha256:fe3eec281222c5778eed6a4185d0442a7d7aaac552039359d5ec4c5b8737baa3"
+                "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.9.2"
+            "version": "==1.12.2"
         },
         "pyopenssl": {
             "hashes": [
-                "sha256:7a83b7b272dd595222d672f5ce29aa030f1fb837630ef229f62e72e395ce8968",
-                "sha256:b28437c9773bb6c6958628cf9c3bebe585de661dba6f63df17111966363dd15e"
+                "sha256:24f0dc5227396b3e831f4c7f602b950a5e9833d292c8e4a2e06b709292806ae2",
+                "sha256:276f931f55a452e7dea69c7173e984eb2a4407ce413c918aa34b55f82f9b8bac"
             ],
             "index": "pypi",
-            "version": "==22.1.0"
+            "version": "==23.2.0"
         },
         "pysocks": {
             "hashes": [
@@ -422,11 +401,11 @@
         },
         "pytest": {
             "hashes": [
-                "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89",
-                "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"
+                "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32",
+                "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"
             ],
             "index": "pypi",
-            "version": "==6.2.5"
+            "version": "==7.4.0"
         },
         "pytest-timeout": {
             "hashes": [
@@ -446,49 +425,49 @@
         },
         "pyyaml": {
             "hashes": [
-                "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf",
-                "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293",
-                "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b",
-                "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57",
-                "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b",
-                "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4",
-                "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07",
-                "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba",
-                "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9",
-                "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287",
-                "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513",
-                "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0",
-                "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782",
-                "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0",
-                "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92",
-                "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f",
-                "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2",
-                "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc",
-                "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1",
-                "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c",
-                "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86",
-                "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4",
-                "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c",
-                "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34",
-                "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b",
-                "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d",
-                "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c",
-                "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb",
-                "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7",
-                "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737",
-                "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3",
-                "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d",
-                "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358",
-                "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53",
-                "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78",
-                "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803",
-                "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a",
-                "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f",
-                "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174",
-                "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"
+                "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"
+            "version": "==6.0.1"
         },
         "requests": {
             "hashes": [
@@ -511,23 +490,16 @@
                 "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7",
                 "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"
             ],
-            "markers": "python_version >= '3.6'",
+            "markers": "python_version >= '3.6' and python_version < '4'",
             "version": "==4.9"
         },
         "selenium": {
             "hashes": [
-                "sha256:ba5b2633f43cf6fe9d308fa4a6996e00a101ab9cb1aad6fd91ae1f3dbe57f56f"
+                "sha256:98e72117b194b3fa9c69b48998f44bf7dd4152c7bd98544911a1753b9f03cc7d",
+                "sha256:9f9a5ed586280a3594f7461eb1d9dab3eac9d91e28572f365e9b98d9d03e02b5"
             ],
             "index": "pypi",
-            "version": "==4.2.0"
-        },
-        "setuptools": {
-            "hashes": [
-                "sha256:5df61bf30bb10c6f756eb19e7c9f3b473051f48db77fddbe06ff2ca307df9a6f",
-                "sha256:62642358adc77ffa87233bc4d2354c4b2682d214048f500964dbe760ccedf102"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==67.8.0"
+            "version": "==4.11.2"
         },
         "six": {
             "hashes": [
@@ -552,35 +524,32 @@
             ],
             "version": "==2.4.0"
         },
-        "toml": {
+        "tomli": {
             "hashes": [
-                "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
-                "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
+                "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
+                "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
             ],
-            "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==0.10.2"
+            "markers": "python_version < '3.11'",
+            "version": "==2.0.1"
         },
         "trio": {
             "hashes": [
-                "sha256:ce68f1c5400a47b137c5a4de72c7c901bd4e7a24fbdebfe9b41de8c6c04eaacf",
-                "sha256:f1dd0780a89bfc880c7c7994519cb53f62aacb2c25ff487001c0052bd721cdf0"
+                "sha256:3887cf18c8bcc894433420305468388dac76932e9668afa1c49aa3806b6accb3",
+                "sha256:f43da357620e5872b3d940a2e3589aa251fd3f881b65a608d742e00809b1ec38"
             ],
             "markers": "python_version >= '3.7'",
-            "version": "==0.22.0"
+            "version": "==0.22.2"
         },
         "trio-websocket": {
             "hashes": [
-                "sha256:0908435e4eecc49d830ae1c4d6c47b978a75f00594a2be2104d58b61a04cdb53",
-                "sha256:af13e9393f9051111300287947ec595d601758ce3d165328e7d36325135a8d62"
+                "sha256:1a748604ad906a7dcab9a43c6eb5681e37de4793ba0847ef0bc9486933ed027b",
+                "sha256:a9937d48e8132ebf833019efde2a52ca82d223a30a7ea3e8d60a7d28f75a4e3a"
             ],
             "markers": "python_version >= '3.7'",
-            "version": "==0.10.2"
+            "version": "==0.10.3"
         },
         "urllib3": {
-            "extras": [
-                "secure",
-                "socks"
-            ],
+            "extras": [],
             "hashes": [
                 "sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f",
                 "sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14"
@@ -588,20 +557,13 @@
             "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"
         },
-        "urllib3-secure-extra": {
-            "hashes": [
-                "sha256:ee9409cbfeb4b8609047be4c32fb4317870c602767e53fd8a41005ebe6a41dff",
-                "sha256:f7adcb108b4d12a4b26b99eb60e265d087f435052a76aefa396b6ee85e9a6ef9"
-            ],
-            "version": "==0.1.0"
-        },
         "websocket-client": {
             "hashes": [
-                "sha256:c7d67c13b928645f259d9b847ab5b57fd2d127213ca41ebd880de1f553b7c23b",
-                "sha256:f8c64e28cd700e7ba1f04350d66422b6833b82a796b525a51e740b8cc8dab4b1"
+                "sha256:c951af98631d24f8df89ab1019fc365f2227c0892f12fd150e935607c79dd0dd",
+                "sha256:f1f9f2ad5291f0225a49efad77abf9e700b6fef553900623060dad6e26503b9d"
             ],
             "markers": "python_version >= '3.7'",
-            "version": "==1.5.2"
+            "version": "==1.6.1"
         },
         "wsproto": {
             "hashes": [
@@ -615,48 +577,55 @@
     "develop": {
         "astroid": {
             "hashes": [
-                "sha256:1efdf4e867d4d8ba4a9f6cf9ce07cd182c4c41de77f23814feb27ca93ca9d877",
-                "sha256:506daabe5edffb7e696ad82483ad0228245a9742ed7d2d8c9cdb31537decf9f6"
+                "sha256:389656ca57b6108f939cf5d2f9a2a825a3be50ba9d589670f393236e0a03b91c",
+                "sha256:903f024859b7c7687d7a7f3a3f73b17301f8e42dfd9cc9df9d4418172d3e2dbd"
             ],
-            "markers": "python_full_version >= '3.6.2'",
-            "version": "==2.9.3"
+            "markers": "python_full_version >= '3.7.2'",
+            "version": "==2.15.6"
         },
         "black": {
             "hashes": [
-                "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b",
-                "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176",
-                "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09",
-                "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a",
-                "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015",
-                "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79",
-                "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb",
-                "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20",
-                "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464",
-                "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968",
-                "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82",
-                "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21",
-                "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0",
-                "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265",
-                "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b",
-                "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a",
-                "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72",
-                "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce",
-                "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0",
-                "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a",
-                "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163",
-                "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad",
-                "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"
+                "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": "==22.3.0"
+            "version": "==23.7.0"
         },
         "click": {
             "hashes": [
-                "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e",
-                "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"
+                "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd",
+                "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"
             ],
             "markers": "python_version >= '3.7'",
-            "version": "==8.1.3"
+            "version": "==8.1.6"
+        },
+        "dill": {
+            "hashes": [
+                "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e",
+                "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"
+            ],
+            "markers": "python_version < '3.11'",
+            "version": "==0.3.7"
         },
         "isort": {
             "hashes": [
@@ -710,10 +679,11 @@
         },
         "mccabe": {
             "hashes": [
-                "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
-                "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
+                "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325",
+                "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"
             ],
-            "version": "==0.6.1"
+            "markers": "python_version >= '3.6'",
+            "version": "==0.7.0"
         },
         "mypy-extensions": {
             "hashes": [
@@ -723,45 +693,37 @@
             "markers": "python_version >= '3.5'",
             "version": "==1.0.0"
         },
-        "pathspec": {
+        "packaging": {
             "hashes": [
-                "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687",
-                "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"
+                "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61",
+                "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"
             ],
             "markers": "python_version >= '3.7'",
-            "version": "==0.11.1"
+            "version": "==23.1"
+        },
+        "pathspec": {
+            "hashes": [
+                "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20",
+                "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==0.11.2"
         },
         "platformdirs": {
             "hashes": [
-                "sha256:412dae91f52a6f84830f39a8078cecd0e866cb72294a5c66808e74d5e88d251f",
-                "sha256:e2378146f1964972c03c085bb5662ae80b2b8c06226c54b2ff4aa9483e8a13a5"
+                "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d",
+                "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"
             ],
             "markers": "python_version >= '3.7'",
-            "version": "==3.5.1"
+            "version": "==3.10.0"
         },
         "pylint": {
             "hashes": [
-                "sha256:9d945a73640e1fec07ee34b42f5669b770c759acd536ec7b16d7e4b87a9c9ff9",
-                "sha256:daabda3f7ed9d1c60f52d563b1b854632fd90035bcf01443e234d3dc794e3b74"
+                "sha256:73995fb8216d3bed149c8d51bba25b2c52a8251a2c8ac846ec668ce38fab5413",
+                "sha256:f7b601cbc06fef7e62a754e2b41294c2aa31f1cb659624b9a85bcba29eaf8252"
             ],
             "index": "pypi",
-            "version": "==2.12.2"
-        },
-        "setuptools": {
-            "hashes": [
-                "sha256:5df61bf30bb10c6f756eb19e7c9f3b473051f48db77fddbe06ff2ca307df9a6f",
-                "sha256:62642358adc77ffa87233bc4d2354c4b2682d214048f500964dbe760ccedf102"
-            ],
-            "markers": "python_version >= '3.7'",
-            "version": "==67.8.0"
-        },
-        "toml": {
-            "hashes": [
-                "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
-                "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
-            ],
-            "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==0.10.2"
+            "version": "==2.17.5"
         },
         "tomli": {
             "hashes": [
@@ -771,70 +733,102 @@
             "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:558bc0c4145f01e6405f4a5fdbd82050bd221b119f4bf72a961a1cfd471349d6",
-                "sha256:6bac751f4789b135c43228e72de18637e9a6c29d12777023a703fd1a6858469f"
+                "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36",
+                "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"
             ],
             "markers": "python_version < '3.10'",
-            "version": "==4.6.1"
+            "version": "==4.7.1"
         },
         "wrapt": {
             "hashes": [
-                "sha256:086218a72ec7d986a3eddb7707c8c4526d677c7b35e355875a0fe2918b059179",
-                "sha256:0877fe981fd76b183711d767500e6b3111378ed2043c145e21816ee589d91096",
-                "sha256:0a017a667d1f7411816e4bf214646d0ad5b1da2c1ea13dec6c162736ff25a374",
-                "sha256:0cb23d36ed03bf46b894cfec777eec754146d68429c30431c99ef28482b5c1df",
-                "sha256:1fea9cd438686e6682271d36f3481a9f3636195578bab9ca3382e2f5f01fc185",
-                "sha256:220a869982ea9023e163ba915077816ca439489de6d2c09089b219f4e11b6785",
-                "sha256:25b1b1d5df495d82be1c9d2fad408f7ce5ca8a38085e2da41bb63c914baadff7",
-                "sha256:2dded5496e8f1592ec27079b28b6ad2a1ef0b9296d270f77b8e4a3a796cf6909",
-                "sha256:2ebdde19cd3c8cdf8df3fc165bc7827334bc4e353465048b36f7deeae8ee0918",
-                "sha256:43e69ffe47e3609a6aec0fe723001c60c65305784d964f5007d5b4fb1bc6bf33",
-                "sha256:46f7f3af321a573fc0c3586612db4decb7eb37172af1bc6173d81f5b66c2e068",
-                "sha256:47f0a183743e7f71f29e4e21574ad3fa95676136f45b91afcf83f6a050914829",
-                "sha256:498e6217523111d07cd67e87a791f5e9ee769f9241fcf8a379696e25806965af",
-                "sha256:4b9c458732450ec42578b5642ac53e312092acf8c0bfce140ada5ca1ac556f79",
-                "sha256:51799ca950cfee9396a87f4a1240622ac38973b6df5ef7a41e7f0b98797099ce",
-                "sha256:5601f44a0f38fed36cc07db004f0eedeaadbdcec90e4e90509480e7e6060a5bc",
-                "sha256:5f223101f21cfd41deec8ce3889dc59f88a59b409db028c469c9b20cfeefbe36",
-                "sha256:610f5f83dd1e0ad40254c306f4764fcdc846641f120c3cf424ff57a19d5f7ade",
-                "sha256:6a03d9917aee887690aa3f1747ce634e610f6db6f6b332b35c2dd89412912bca",
-                "sha256:705e2af1f7be4707e49ced9153f8d72131090e52be9278b5dbb1498c749a1e32",
-                "sha256:766b32c762e07e26f50d8a3468e3b4228b3736c805018e4b0ec8cc01ecd88125",
-                "sha256:77416e6b17926d953b5c666a3cb718d5945df63ecf922af0ee576206d7033b5e",
-                "sha256:778fd096ee96890c10ce96187c76b3e99b2da44e08c9e24d5652f356873f6709",
-                "sha256:78dea98c81915bbf510eb6a3c9c24915e4660302937b9ae05a0947164248020f",
-                "sha256:7dd215e4e8514004c8d810a73e342c536547038fb130205ec4bba9f5de35d45b",
-                "sha256:7dde79d007cd6dfa65afe404766057c2409316135cb892be4b1c768e3f3a11cb",
-                "sha256:81bd7c90d28a4b2e1df135bfbd7c23aee3050078ca6441bead44c42483f9ebfb",
-                "sha256:85148f4225287b6a0665eef08a178c15097366d46b210574a658c1ff5b377489",
-                "sha256:865c0b50003616f05858b22174c40ffc27a38e67359fa1495605f96125f76640",
-                "sha256:87883690cae293541e08ba2da22cacaae0a092e0ed56bbba8d018cc486fbafbb",
-                "sha256:8aab36778fa9bba1a8f06a4919556f9f8c7b33102bd71b3ab307bb3fecb21851",
-                "sha256:8c73c1a2ec7c98d7eaded149f6d225a692caa1bd7b2401a14125446e9e90410d",
-                "sha256:936503cb0a6ed28dbfa87e8fcd0a56458822144e9d11a49ccee6d9a8adb2ac44",
-                "sha256:944b180f61f5e36c0634d3202ba8509b986b5fbaf57db3e94df11abee244ba13",
-                "sha256:96b81ae75591a795d8c90edc0bfaab44d3d41ffc1aae4d994c5aa21d9b8e19a2",
-                "sha256:981da26722bebb9247a0601e2922cedf8bb7a600e89c852d063313102de6f2cb",
-                "sha256:ae9de71eb60940e58207f8e71fe113c639da42adb02fb2bcbcaccc1ccecd092b",
-                "sha256:b73d4b78807bd299b38e4598b8e7bd34ed55d480160d2e7fdaabd9931afa65f9",
-                "sha256:d4a5f6146cfa5c7ba0134249665acd322a70d1ea61732723c7d3e8cc0fa80755",
-                "sha256:dd91006848eb55af2159375134d724032a2d1d13bcc6f81cd8d3ed9f2b8e846c",
-                "sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a",
-                "sha256:e6906d6f48437dfd80464f7d7af1740eadc572b9f7a4301e7dd3d65db285cacf",
-                "sha256:e92d0d4fa68ea0c02d39f1e2f9cb5bc4b4a71e8c442207433d8db47ee79d7aa3",
-                "sha256:e94b7d9deaa4cc7bac9198a58a7240aaf87fe56c6277ee25fa5b3aa1edebd229",
-                "sha256:ea3e746e29d4000cd98d572f3ee2a6050a4f784bb536f4ac1f035987fc1ed83e",
-                "sha256:ec7e20258ecc5174029a0f391e1b948bf2906cd64c198a9b8b281b811cbc04de",
-                "sha256:ec9465dd69d5657b5d2fa6133b3e1e989ae27d29471a672416fd729b429eb554",
-                "sha256:f122ccd12fdc69628786d0c947bdd9cb2733be8f800d88b5a37c57f1f1d73c10",
-                "sha256:f99c0489258086308aad4ae57da9e8ecf9e1f3f30fa35d5e170b4d4896554d80",
-                "sha256:f9c51d9af9abb899bd34ace878fbec8bf357b3194a10c4e8e0a25512826ef056",
-                "sha256:fd76c47f20984b43d93de9a82011bb6e5f8325df6c9ed4d8310029a55fa361ea"
+                "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 >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
-            "version": "==1.13.3"
+            "markers": "python_version < '3.11'",
+            "version": "==1.15.0"
         }
     }
 }
diff --git a/container-images/gerrit-base/Dockerfile b/container-images/gerrit-base/Dockerfile
index 26f47a7..10e3c07 100644
--- a/container-images/gerrit-base/Dockerfile
+++ b/container-images/gerrit-base/Dockerfile
@@ -11,11 +11,14 @@
 RUN mkdir -p /var/gerrit/bin && \
     mkdir -p /var/gerrit/etc && \
     mkdir -p /var/gerrit/plugins && \
-    mkdir -p /var/plugins && \
+    mkdir -p /var/plugins/ha && \
+    mkdir -p /var/plugins/zookeeper && \
+    mkdir -p /var/libs/ha && \
     mkdir -p /var/war
 
 # Download Gerrit release
-ARG GERRIT_WAR_URL=https://gerrit-releases.storage.googleapis.com/gerrit-3.8.0.war
+# 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
 
@@ -24,6 +27,20 @@
 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/libs/ha/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/ha/high-availability.jar ${HA_JAR_URL} && \
+    cp /var/plugins/ha/high-availability.jar var/libs/ha/high-availability.jar
+
+# Download zookeeper-refdb plugin
+#ARG ZOOKEEPER_REFDB_URL=https://gerrit-ci.gerritforge.com/view/Plugins-stable-3.8/job/plugin-zookeeper-refdb-bazel-master-stable-3.8/lastSuccessfulBuild/artifact/bazel-bin/plugins/zookeeper-refdb/zookeeper-refdb.jar
+ARG ZOOKEEPER_REFDB_URL=https://gerrit-ci.gerritforge.com/view/Plugins-stable-3.7/job/plugin-zookeeper-refdb-bazel-stable-3.7/lastSuccessfulBuild/artifact/bazel-bin/plugins/zookeeper-refdb/zookeeper-refdb.jar
+RUN curl -k -o /var/plugins/zookeeper/zookeeper-refdb.jar ${ZOOKEEPER_REFDB_URL}
+
 # Allow incoming traffic
 EXPOSE 29418 8080
 
diff --git a/container-images/gerrit-init/README.md b/container-images/gerrit-init/README.md
index caeb030..37b5bda 100644
--- a/container-images/gerrit-init/README.md
+++ b/container-images/gerrit-init/README.md
@@ -28,3 +28,37 @@
 `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/container-images/gerrit-init/tools/gerrit-initializer/initializer/config/init_config.py b/container-images/gerrit-init/tools/gerrit-initializer/initializer/config/init_config.py
index 13d960f..fe58154 100644
--- a/container-images/gerrit-init/tools/gerrit-initializer/initializer/config/init_config.py
+++ b/container-images/gerrit-init/tools/gerrit-initializer/initializer/config/init_config.py
@@ -19,14 +19,16 @@
 
 class InitConfig:
     def __init__(self):
-        self.downloaded_plugins = []
+        self.plugins = []
+        self.libs = []
         self.plugin_cache_enabled = False
-        self.packaged_plugins = set()
-        self.install_as_library = set()
         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}")
@@ -37,23 +39,49 @@
         if config is None:
             raise ValueError(f"Invalid config-file: {config_file}")
 
-        if "downloadedPlugins" in config:
-            self.downloaded_plugins = config["downloadedPlugins"]
-        if "packagedPlugins" in config:
-            self.packaged_plugins = set(config["packagedPlugins"])
-        if "installAsLibrary" in config:
-            self.install_as_library = set(config["installAsLibrary"])
+        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_all_configured_plugins(self):
-        plugins = set(self.packaged_plugins)
-        plugins.update([p["name"] for p in self.downloaded_plugins])
-        return plugins
+    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_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/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/download_plugins.py b/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/download_plugins.py
index ec1ec67..c107e70 100755
--- a/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/download_plugins.py
+++ b/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/download_plugins.py
@@ -41,6 +41,7 @@
         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")
@@ -51,46 +52,98 @@
             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):
-        if os.path.exists(self.plugin_dir):
-            return [f for f in os.listdir(self.plugin_dir) if f.endswith(".jar")]
+        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 = self._get_required_jars("/var/plugins", self.config.get_plugins())
+        if self.config.is_ha:
+            required.extend(
+                self._get_required_jars("/var/plugins/ha", self.config.get_plugins())
+            )
+        if self.config.refdb:
+            refdb_path = f"/var/plugins/{self.config.refdb}"
+            if not os.path.exists(refdb_path):
+                raise FileNotFoundError(
+                    "Invalid refdb. Unable to find refdb plugin in container."
+                )
+            required.extend(
+                self._get_required_jars(
+                    f"/var/plugins/{self.config.refdb}", self.config.get_plugins()
+                )
+            )
+        LOG.info("Requiring plugins: %s", required)
+        return required
+
+    def _get_required_libs(self):
+        required = self._get_required_jars("/var/libs", self.config.get_libs())
+        if self.config.is_ha:
+            required.extend(
+                self._get_required_jars("/var/libs/ha", self.config.get_libs())
+            )
+        LOG.info("Requiring libs: %s", required)
+        return required
+
+    @staticmethod
+    def _get_required_jars(dir, configured_plugins):
         required = [
-            os.path.splitext(f)[0]
-            for f in os.listdir("/var/plugins")
+            {"name": os.path.splitext(f)[0], "source_path": os.path.join(dir, f)}
+            for f in os.listdir(dir)
             if f.endswith(".jar")
         ]
-        return list(
-            filter(
-                lambda x: x not in self.config.get_all_configured_plugins(), required
-            )
-        )
+        return list(filter(lambda x: x not in configured_plugins, required))
 
     def _install_plugins_from_container(self):
-        source_dir = "/var/plugins"
-        for plugin in self.required_plugins:
-            source_file = os.path.join(source_dir, plugin + ".jar")
-            target_file = os.path.join(self.plugin_dir, plugin + ".jar")
+        self._install_jars_from_container(self.required_plugins, self.plugin_dir)
+
+    def _install_libs_from_container(self):
+        self._install_jars_from_container(self.required_libs, self.lib_dir)
+
+    def _install_jars_from_container(self, plugins, target_dir):
+        for plugin in plugins:
+            target_file = os.path.join(target_dir, plugin["name"] + ".jar")
+            LOG.info(
+                "Installing plugin %s from container to %s.",
+                plugin["name"],
+                target_file,
+            )
+            if not os.path.exists(plugin["source_path"]):
+                raise FileNotFoundError(
+                    "Unable to find required plugin in container: " + plugin["name"]
+                )
             if os.path.exists(target_file) and self._get_file_sha(
-                source_file
+                plugin["source_path"]
             ) == self._get_file_sha(target_file):
                 continue
 
-            shutil.copyfile(source_file, target_file)
+            shutil.copyfile(plugin["source_path"], target_file)
             self.plugins_changed = True
 
     def _install_plugins_from_war(self):
-        for plugin in self.config.packaged_plugins:
-            LOG.info("Installing packaged plugin %s.", plugin)
+        for plugin in self.config.get_packaged_plugins():
+            plugin_name = plugin["name"]
+            LOG.info("Installing packaged plugin %s.", plugin_name)
             with ZipFile("/var/war/gerrit.war", "r") as war:
-                war.extract(f"WEB-INF/plugins/{plugin}.jar", self.plugin_dir)
+                war.extract(f"WEB-INF/plugins/{plugin_name}.jar", self.plugin_dir)
 
             os.rename(
-                f"{self.plugin_dir}/WEB-INF/plugins/{plugin}.jar",
-                os.path.join(self.plugin_dir, f"{plugin}.jar"),
+                f"{self.plugin_dir}/WEB-INF/plugins/{plugin_name}.jar",
+                os.path.join(self.plugin_dir, f"{plugin_name}.jar"),
             )
         shutil.rmtree(os.path.join(self.plugin_dir, "WEB-INF"), ignore_errors=True)
 
@@ -109,11 +162,23 @@
         return file_hash.hexdigest()
 
     def _remove_unwanted_plugins(self):
-        wanted_plugins = list(self.config.get_all_configured_plugins())
+        wanted_plugins = list(self.config.get_plugins())
         wanted_plugins.extend(self.required_plugins)
-        for plugin in self._get_installed_plugins():
-            if os.path.splitext(plugin)[0] not in wanted_plugins:
-                os.remove(os.path.join(self.plugin_dir, plugin))
+        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):
@@ -124,11 +189,12 @@
                 path = os.path.join(self.lib_dir, f)
                 if (
                     os.path.islink(path)
-                    and os.path.splitext(f)[0] not in self.config.install_as_library
+                    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.install_as_library:
+        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:
@@ -142,13 +208,22 @@
 
     def execute(self):
         self._create_plugins_dir()
+        self._create_lib_dir()
+
         self._remove_unwanted_plugins()
+        self._remove_unwanted_libs()
+
         self._install_plugins_from_container()
+        self._install_libs_from_container()
+
         self._install_plugins_from_war()
 
-        for plugin in self.config.downloaded_plugins:
+        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):
@@ -173,14 +248,20 @@
                 )
             )
 
-    @abstractmethod
     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_plugin(self, plugin):
-        target = os.path.join(self.plugin_dir, f"{plugin['name']}.jar")
+    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
 
@@ -255,8 +336,8 @@
         shutil.copy(cached_plugin_path, target)
         self._cleanup_cache(os.path.dirname(cached_plugin_path))
 
-    def _install_plugin(self, plugin):
-        install_path = os.path.join(self.plugin_dir, f"{plugin['name']}.jar")
+    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"]
diff --git a/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/init.py b/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/init.py
index 8266f6f..4931984 100755
--- a/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/init.py
+++ b/container-images/gerrit-init/tools/gerrit-initializer/initializer/tasks/init.py
@@ -96,7 +96,7 @@
             LOG.info("Plugins were installed or updated. Initializing.")
             return True
 
-        if self.config.packaged_plugins.difference(self.installed_plugins):
+        if self.config.get_plugin_names().difference(self.installed_plugins):
             LOG.info("Reininitializing site to install additional plugins.")
             return True
 
@@ -122,6 +122,10 @@
         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")
@@ -130,9 +134,8 @@
         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))
+                if os.path.islink(abs_path) and not os.path.exists(
+                    os.path.realpath(abs_path)
                 ):
                     os.unlink(abs_path)
         else:
@@ -162,6 +165,16 @@
                             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()
@@ -205,6 +218,7 @@
                 )
                 sys.exit(1)
 
+            self._remove_auto_generated_ssh_keys()
             self._symlink_configuration()
 
             if self.is_replica:
diff --git a/container-images/gerrit/tools/start b/container-images/gerrit/tools/start
index e5e3d3f..2073181 100755
--- a/container-images/gerrit/tools/start
+++ b/container-images/gerrit/tools/start
@@ -7,6 +7,7 @@
 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/helm-charts/gerrit-operator-crds/templates/gerritclusters.gerritoperator.google.com-v1.yml b/helm-charts/gerrit-operator-crds/templates/gerritclusters.gerritoperator.google.com-v1.yml
index c906d5e..666c7f0 100644
--- a/helm-charts/gerrit-operator-crds/templates/gerritclusters.gerritoperator.google.com-v1.yml
+++ b/helm-charts/gerrit-operator-crds/templates/gerritclusters.gerritoperator.google.com-v1.yml
@@ -13,7 +13,7 @@
     singular: gerritcluster
   scope: Namespaced
   versions:
-  - name: v1alpha3
+  - name: v1alpha14
     schema:
       openAPIV3Schema:
         properties:
@@ -21,6 +21,11 @@
             properties:
               storage:
                 properties:
+                  pluginCache:
+                    properties:
+                      enabled:
+                        type: boolean
+                    type: object
                   storageClasses:
                     properties:
                       readWriteOnce:
@@ -37,70 +42,15 @@
                             type: string
                         type: object
                     type: object
-                  gitRepositoryStorage:
+                  sharedStorage:
                     properties:
-                      size:
-                        anyOf:
-                        - type: integer
-                        - type: string
-                        x-kubernetes-int-or-string: true
-                      volumeName:
-                        type: string
-                      selector:
+                      externalPVC:
                         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
+                          enabled:
+                            type: boolean
+                          claimName:
+                            type: string
                         type: object
-                    type: object
-                  logsStorage:
-                    properties:
-                      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
-                  pluginCacheStorage:
-                    properties:
-                      enabled:
-                        type: boolean
                       size:
                         anyOf:
                         - type: integer
@@ -162,12 +112,6 @@
                 properties:
                   enabled:
                     type: boolean
-                  type:
-                    enum:
-                    - NONE
-                    - INGRESS
-                    - ISTIO
-                    type: string
                   host:
                     type: string
                   annotations:
@@ -181,7 +125,29 @@
                       secret:
                         type: string
                     type: object
+                  ssh:
+                    properties:
+                      enabled:
+                        type: boolean
+                    type: object
                 type: object
+              refdb:
+                properties:
+                  database:
+                    enum:
+                    - NONE
+                    - ZOOKEEPER
+                    type: string
+                  zookeeper:
+                    properties:
+                      connectString:
+                        type: string
+                      rootNode:
+                        type: string
+                    type: object
+                type: object
+              serverId:
+                type: string
               gerrits:
                 items:
                   properties:
@@ -258,6 +224,8 @@
                       type: object
                     spec:
                       properties:
+                        serviceAccount:
+                          type: string
                         tolerations:
                           items:
                             properties:
@@ -830,12 +798,12 @@
                           type: integer
                         service:
                           properties:
+                            sshPort:
+                              type: integer
                             type:
                               type: string
                             httpPort:
                               type: integer
-                            sshPort:
-                              type: integer
                           type: object
                         site:
                           properties:
@@ -848,70 +816,25 @@
                         plugins:
                           items:
                             properties:
+                              installAsLibrary:
+                                type: boolean
                               name:
                                 type: string
                               url:
-                                properties:
-                                  protocol:
-                                    type: string
-                                  host:
-                                    type: string
-                                  port:
-                                    type: integer
-                                  file:
-                                    type: string
-                                  query:
-                                    type: string
-                                  authority:
-                                    type: string
-                                  path:
-                                    type: string
-                                  userInfo:
-                                    type: string
-                                  ref:
-                                    type: string
-                                  hostAddress:
-                                    properties:
-                                      holder:
-                                        properties:
-                                          originalHostName:
-                                            type: string
-                                          hostName:
-                                            type: string
-                                          address:
-                                            type: integer
-                                          family:
-                                            type: integer
-                                        type: object
-                                      canonicalHostName:
-                                        type: string
-                                    type: object
-                                  handler:
-                                    type: object
-                                  hashCode:
-                                    type: integer
-                                  tempState:
-                                    properties:
-                                      protocol:
-                                        type: string
-                                      host:
-                                        type: string
-                                      port:
-                                        type: integer
-                                      authority:
-                                        type: string
-                                      file:
-                                        type: string
-                                      ref:
-                                        type: string
-                                      hashCode:
-                                        type: integer
-                                    type: object
-                                type: object
+                                type: string
                               sha1:
                                 type: string
-                              installAsLibrary:
-                                type: boolean
+                            type: object
+                          type: array
+                        libs:
+                          items:
+                            properties:
+                              name:
+                                type: string
+                              url:
+                                type: string
+                              sha1:
+                                type: string
                             type: object
                           type: array
                         configFiles:
@@ -925,6 +848,13 @@
                           - PRIMARY
                           - REPLICA
                           type: string
+                        debug:
+                          properties:
+                            enabled:
+                              type: boolean
+                            suspend:
+                              type: boolean
+                          type: object
                       type: object
                   type: object
                 type: array
diff --git a/helm-charts/gerrit-operator-crds/templates/gerritnetworks.gerritoperator.google.com-v1.yml b/helm-charts/gerrit-operator-crds/templates/gerritnetworks.gerritoperator.google.com-v1.yml
new file mode 100644
index 0000000..d27bf31
--- /dev/null
+++ b/helm-charts/gerrit-operator-crds/templates/gerritnetworks.gerritoperator.google.com-v1.yml
@@ -0,0 +1,125 @@
+# 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: v1alpha1
+    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
+                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/helm-charts/gerrit-operator-crds/templates/gerrits.gerritoperator.google.com-v1.yml b/helm-charts/gerrit-operator-crds/templates/gerrits.gerritoperator.google.com-v1.yml
index 89b83eb..db0141e 100644
--- a/helm-charts/gerrit-operator-crds/templates/gerrits.gerritoperator.google.com-v1.yml
+++ b/helm-charts/gerrit-operator-crds/templates/gerrits.gerritoperator.google.com-v1.yml
@@ -13,7 +13,7 @@
     singular: gerrit
   scope: Namespaced
   versions:
-  - name: v1alpha4
+  - name: v1alpha16
     schema:
       openAPIV3Schema:
         properties:
@@ -49,6 +49,11 @@
                 type: object
               storage:
                 properties:
+                  pluginCache:
+                    properties:
+                      enabled:
+                        type: boolean
+                    type: object
                   storageClasses:
                     properties:
                       readWriteOnce:
@@ -65,70 +70,15 @@
                             type: string
                         type: object
                     type: object
-                  gitRepositoryStorage:
+                  sharedStorage:
                     properties:
-                      size:
-                        anyOf:
-                        - type: integer
-                        - type: string
-                        x-kubernetes-int-or-string: true
-                      volumeName:
-                        type: string
-                      selector:
+                      externalPVC:
                         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
+                          enabled:
+                            type: boolean
+                          claimName:
+                            type: string
                         type: object
-                    type: object
-                  logsStorage:
-                    properties:
-                      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
-                  pluginCacheStorage:
-                    properties:
-                      enabled:
-                        type: boolean
                       size:
                         anyOf:
                         - type: integer
@@ -160,17 +110,37 @@
                 type: object
               ingress:
                 properties:
-                  type:
-                    enum:
-                    - NONE
-                    - INGRESS
-                    - ISTIO
-                    type: string
+                  enabled:
+                    type: boolean
                   host:
                     type: string
                   tlsEnabled:
                     type: boolean
+                  ssh:
+                    properties:
+                      enabled:
+                        type: boolean
+                    type: object
                 type: object
+              refdb:
+                properties:
+                  database:
+                    enum:
+                    - NONE
+                    - ZOOKEEPER
+                    type: string
+                  zookeeper:
+                    properties:
+                      connectString:
+                        type: string
+                      rootNode:
+                        type: string
+                    type: object
+                type: object
+              serverId:
+                type: string
+              serviceAccount:
+                type: string
               tolerations:
                 items:
                   properties:
@@ -743,12 +713,12 @@
                 type: integer
               service:
                 properties:
+                  sshPort:
+                    type: integer
                   type:
                     type: string
                   httpPort:
                     type: integer
-                  sshPort:
-                    type: integer
                 type: object
               site:
                 properties:
@@ -761,70 +731,25 @@
               plugins:
                 items:
                   properties:
+                    installAsLibrary:
+                      type: boolean
                     name:
                       type: string
                     url:
-                      properties:
-                        protocol:
-                          type: string
-                        host:
-                          type: string
-                        port:
-                          type: integer
-                        file:
-                          type: string
-                        query:
-                          type: string
-                        authority:
-                          type: string
-                        path:
-                          type: string
-                        userInfo:
-                          type: string
-                        ref:
-                          type: string
-                        hostAddress:
-                          properties:
-                            holder:
-                              properties:
-                                originalHostName:
-                                  type: string
-                                hostName:
-                                  type: string
-                                address:
-                                  type: integer
-                                family:
-                                  type: integer
-                              type: object
-                            canonicalHostName:
-                              type: string
-                          type: object
-                        handler:
-                          type: object
-                        hashCode:
-                          type: integer
-                        tempState:
-                          properties:
-                            protocol:
-                              type: string
-                            host:
-                              type: string
-                            port:
-                              type: integer
-                            authority:
-                              type: string
-                            file:
-                              type: string
-                            ref:
-                              type: string
-                            hashCode:
-                              type: integer
-                          type: object
-                      type: object
+                      type: string
                     sha1:
                       type: string
-                    installAsLibrary:
-                      type: boolean
+                  type: object
+                type: array
+              libs:
+                items:
+                  properties:
+                    name:
+                      type: string
+                    url:
+                      type: string
+                    sha1:
+                      type: string
                   type: object
                 type: array
               configFiles:
@@ -838,6 +763,13 @@
                 - PRIMARY
                 - REPLICA
                 type: string
+              debug:
+                properties:
+                  enabled:
+                    type: boolean
+                  suspend:
+                    type: boolean
+                type: object
             type: object
           status:
             properties:
diff --git a/helm-charts/gerrit-operator-crds/templates/receivers.gerritoperator.google.com-v1.yml b/helm-charts/gerrit-operator-crds/templates/receivers.gerritoperator.google.com-v1.yml
index b357650..c314670 100644
--- a/helm-charts/gerrit-operator-crds/templates/receivers.gerritoperator.google.com-v1.yml
+++ b/helm-charts/gerrit-operator-crds/templates/receivers.gerritoperator.google.com-v1.yml
@@ -13,7 +13,7 @@
     singular: receiver
   scope: Namespaced
   versions:
-  - name: v1alpha2
+  - name: v1alpha6
     schema:
       openAPIV3Schema:
         properties:
@@ -65,70 +65,15 @@
                             type: string
                         type: object
                     type: object
-                  gitRepositoryStorage:
+                  sharedStorage:
                     properties:
-                      size:
-                        anyOf:
-                        - type: integer
-                        - type: string
-                        x-kubernetes-int-or-string: true
-                      volumeName:
-                        type: string
-                      selector:
+                      externalPVC:
                         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
+                          enabled:
+                            type: boolean
+                          claimName:
+                            type: string
                         type: object
-                    type: object
-                  logsStorage:
-                    properties:
-                      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
-                  pluginCacheStorage:
-                    properties:
-                      enabled:
-                        type: boolean
                       size:
                         anyOf:
                         - type: integer
@@ -160,16 +105,17 @@
                 type: object
               ingress:
                 properties:
-                  type:
-                    enum:
-                    - NONE
-                    - INGRESS
-                    - ISTIO
-                    type: string
+                  enabled:
+                    type: boolean
                   host:
                     type: string
                   tlsEnabled:
                     type: boolean
+                  ssh:
+                    properties:
+                      enabled:
+                        type: boolean
+                    type: object
                 type: object
               tolerations:
                 items:
diff --git a/helm-charts/gerrit-operator/templates/operator.yaml b/helm-charts/gerrit-operator/templates/operator.yaml
index 85cc76c..f9ed84f 100644
--- a/helm-charts/gerrit-operator/templates/operator.yaml
+++ b/helm-charts/gerrit-operator/templates/operator.yaml
@@ -41,6 +41,8 @@
           valueFrom:
             fieldRef:
               fieldPath: metadata.namespace
+        - name: INGRESS
+          value: {{ .Values.ingress.type }}
         ports:
         - containerPort: 80
         readinessProbe:
diff --git a/helm-charts/gerrit-operator/values.yaml b/helm-charts/gerrit-operator/values.yaml
index 7e40223..ebb88af 100644
--- a/helm-charts/gerrit-operator/values.yaml
+++ b/helm-charts/gerrit-operator/values.yaml
@@ -7,6 +7,10 @@
   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:
diff --git a/helm-charts/gerrit-replica/README.md b/helm-charts/gerrit-replica/README.md
index 547bf0b..993f4d9 100644
--- a/helm-charts/gerrit-replica/README.md
+++ b/helm-charts/gerrit-replica/README.md
@@ -423,14 +423,17 @@
 | `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.plugins.packaged` | List of Gerrit plugins that are packaged into the Gerrit-war-file to install | `["commit-message-length-validator", "download-commands", "replication", "reviewnotes"]` |
-| `gerritReplica.plugins.downloaded` | List of Gerrit plugins that will be downloaded | `nil` |
-| `gerritReplica.plugins.downloaded[0].name` | Name of plugin | `nil` |
-| `gerritReplica.plugins.downloaded[0].url` | Download url of plugin | `nil` |
-| `gerritReplica.plugins.downloaded[0].sha1` | SHA1 sum of plugin jar used to ensure file integrity and version (optional) | `nil` |
-| `gerritReplica.plugins.installAsLibrary` | List of plugins, which should be symlinked to the lib-dir in the Gerrit site (have to be in either `..downloaded` or `..packaged`) | `[]` |
-| `gerritReplica.plugins.cache.enabled` | Whether to cache downloaded plugins | `false` |
-| `gerritReplica.plugins.cache.size` | Size of the volume used to store cached plugins | `1Gi` |
+| `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) |
diff --git a/helm-charts/gerrit-replica/templates/gerrit-replica.configmap.yaml b/helm-charts/gerrit-replica/templates/gerrit-replica.configmap.yaml
index f4f5537..1aa9496 100644
--- a/helm-charts/gerrit-replica/templates/gerrit-replica.configmap.yaml
+++ b/helm-charts/gerrit-replica/templates/gerrit-replica.configmap.yaml
@@ -46,19 +46,15 @@
     {{ if .Values.caCert -}}
     caCertPath: /var/config/ca.crt
     {{- end }}
-    pluginCache: {{ .Values.gerritReplica.plugins.cache.enabled }}
+    pluginCacheEnabled: {{ .Values.gerritReplica.pluginManagement.cache.enabled }}
     pluginCacheDir: /var/mnt/plugins
-    {{- if .Values.gerritReplica.plugins.packaged }}
-    packagedPlugins:
-{{ toYaml .Values.gerritReplica.plugins.packaged | indent 6}}
+    {{- if .Values.gerritReplica.pluginManagement.plugins }}
+    plugins:
+{{ toYaml .Values.gerritReplica.pluginManagement.plugins | indent 6}}
     {{- end }}
-    {{- if .Values.gerritReplica.plugins.downloaded }}
-    downloadedPlugins:
-{{ toYaml .Values.gerritReplica.plugins.downloaded | indent 6 }}
-    {{- end }}
-    {{- if .Values.gerritReplica.plugins.installAsLibrary }}
-    installAsLibrary:
-{{ toYaml .Values.gerritReplica.plugins.installAsLibrary | indent 6 }}
+    {{- if .Values.gerritReplica.pluginManagement.libs }}
+    libs:
+{{ toYaml .Values.gerritReplica.pluginManagement.libs | indent 6}}
     {{- end }}
 {{- range .Values.gerritReplica.additionalConfigMaps -}}
 {{- if .data }}
diff --git a/helm-charts/gerrit-replica/templates/gerrit-replica.stateful-set.yaml b/helm-charts/gerrit-replica/templates/gerrit-replica.stateful-set.yaml
index 901bc50..d4d74a9 100644
--- a/helm-charts/gerrit-replica/templates/gerrit-replica.stateful-set.yaml
+++ b/helm-charts/gerrit-replica/templates/gerrit-replica.stateful-set.yaml
@@ -124,7 +124,7 @@
           mountPath: "/etc/idmapd.conf"
           subPath: idmapd.conf
         {{- end }}
-        {{- if and .Values.gerritReplica.plugins.cache.enabled .Values.gerritReplica.plugins.downloaded }}
+        {{- if and .Values.gerritReplica.pluginManagement.cache.enabled }}
         - name: gerrit-plugin-cache
           mountPath: "/var/mnt/plugins"
         {{- end }}
@@ -259,7 +259,7 @@
       - name: gerrit-site
         emptyDir: {}
       {{- end }}
-      {{- if and .Values.gerritReplica.plugins.cache.enabled .Values.gerritReplica.plugins.downloaded }}
+      {{- if and .Values.gerritReplica.pluginManagement.cache.enabled }}
       - name: gerrit-plugin-cache
         persistentVolumeClaim:
           claimName: {{ .Release.Name }}-plugin-cache-pvc
diff --git a/helm-charts/gerrit-replica/templates/gerrit-replica.storage.yaml b/helm-charts/gerrit-replica/templates/gerrit-replica.storage.yaml
index 9283c8b..c710737 100644
--- a/helm-charts/gerrit-replica/templates/gerrit-replica.storage.yaml
+++ b/helm-charts/gerrit-replica/templates/gerrit-replica.storage.yaml
@@ -1,4 +1,4 @@
-{{- if and .Values.gerritReplica.plugins.cache.enabled .Values.gerritReplica.plugins.downloaded }}
+{{- if and .Values.gerritReplica.pluginManagement.cache.enabled }}
 kind: PersistentVolumeClaim
 apiVersion: v1
 metadata:
@@ -17,6 +17,6 @@
   - ReadWriteMany
   resources:
     requests:
-      storage: {{ .Values.gerritReplica.plugins.cache.size }}
+      storage: {{ .Values.gerritReplica.pluginManagement.cache.size }}
   storageClassName: {{ .Values.storageClasses.shared.name }}
 {{- end }}
diff --git a/helm-charts/gerrit-replica/values.yaml b/helm-charts/gerrit-replica/values.yaml
index 4b19ae8..3f318f8 100644
--- a/helm-charts/gerrit-replica/values.yaml
+++ b/helm-charts/gerrit-replica/values.yaml
@@ -336,16 +336,25 @@
   # automatic encoding using helm does not work here.
   keystore:
 
-  plugins:
-    packaged:
-    - singleusergroup
-    downloaded:
+  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: []
+    #   installAsLibrary: false
+
     # Only downloaded plugins will be cached. This will be ignored, if no plugins
     # are downloaded.
+    libs: []
     cache:
       enabled: false
       size: 1Gi
diff --git a/helm-charts/gerrit/README.md b/helm-charts/gerrit/README.md
index 7ee882d..110383a 100644
--- a/helm-charts/gerrit/README.md
+++ b/helm-charts/gerrit/README.md
@@ -325,14 +325,17 @@
 | `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.plugins.packaged` | List of Gerrit plugins that are packaged into the Gerrit-war-file to install | `["commit-message-length-validator", "download-commands", "replication", "reviewnotes"]` |
-| `gerrit.plugins.downloaded` | List of Gerrit plugins that will be downloaded | `nil` |
-| `gerrit.plugins.downloaded[0].name` | Name of plugin | `nil` |
-| `gerrit.plugins.downloaded[0].url` | Download url of plugin | `nil` |
-| `gerrit.plugins.downloaded[0].sha1` | SHA1 sum of plugin jar used to ensure file integrity and version (optional) | `nil` |
-| `gerrit.plugins.installAsLibrary` | List of plugins, which should be symlinked to the lib-dir in the Gerrit site (have to be in either `..downloaded` or `..packaged`) | `[]` |
-| `gerrit.plugins.cache.enabled` | Whether to cache downloaded plugins | `false` |
-| `gerrit.plugins.cache.size` | Size of the volume used to store cached plugins | `1Gi` |
+| `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) |
diff --git a/helm-charts/gerrit/templates/gerrit.configmap.yaml b/helm-charts/gerrit/templates/gerrit.configmap.yaml
index c326bab..83c188c 100644
--- a/helm-charts/gerrit/templates/gerrit.configmap.yaml
+++ b/helm-charts/gerrit/templates/gerrit.configmap.yaml
@@ -46,19 +46,15 @@
     {{ if .Values.caCert -}}
     caCertPath: /var/config/ca.crt
     {{- end }}
-    pluginCache: {{ .Values.gerrit.plugins.cache.enabled }}
+    pluginCacheEnabled: {{ .Values.gerrit.pluginManagement.cache.enabled }}
     pluginCacheDir: /var/mnt/plugins
-    {{- if .Values.gerrit.plugins.packaged }}
-    packagedPlugins:
-{{ toYaml .Values.gerrit.plugins.packaged | indent 6}}
+    {{- if .Values.gerrit.pluginManagement.plugins }}
+    plugins:
+{{ toYaml .Values.gerrit.pluginManagement.plugins | indent 6}}
     {{- end }}
-    {{- if .Values.gerrit.plugins.downloaded }}
-    downloadedPlugins:
-{{ toYaml .Values.gerrit.plugins.downloaded | indent 6 }}
-    {{- end }}
-    {{- if .Values.gerrit.plugins.installAsLibrary }}
-    installAsLibrary:
-{{ toYaml .Values.gerrit.plugins.installAsLibrary | indent 6 }}
+    {{- if .Values.gerrit.pluginManagement.libs }}
+    libs:
+{{ toYaml .Values.gerrit.pluginManagement.libs | indent 6}}
     {{- end }}
 {{- range .Values.gerrit.additionalConfigMaps -}}
 {{- if .data }}
diff --git a/helm-charts/gerrit/templates/gerrit.stateful-set.yaml b/helm-charts/gerrit/templates/gerrit.stateful-set.yaml
index 2f154d8..a02b69e 100644
--- a/helm-charts/gerrit/templates/gerrit.stateful-set.yaml
+++ b/helm-charts/gerrit/templates/gerrit.stateful-set.yaml
@@ -124,7 +124,7 @@
           mountPath: "/etc/idmapd.conf"
           subPath: idmapd.conf
         {{- end }}
-        {{- if and .Values.gerrit.plugins.cache.enabled .Values.gerrit.plugins.downloaded }}
+        {{- if and .Values.gerrit.pluginManagement.cache.enabled }}
         - name: gerrit-plugin-cache
           mountPath: "/var/mnt/plugins"
         {{- end }}
@@ -215,7 +215,7 @@
       - name: gerrit-site
         emptyDir: {}
       {{- end }}
-      {{- if and .Values.gerrit.plugins.cache.enabled .Values.gerrit.plugins.downloaded }}
+      {{- if and .Values.gerrit.pluginManagement.cache.enabled }}
       - name: gerrit-plugin-cache
         persistentVolumeClaim:
           claimName: {{ .Release.Name }}-plugin-cache-pvc
diff --git a/helm-charts/gerrit/templates/gerrit.storage.yaml b/helm-charts/gerrit/templates/gerrit.storage.yaml
index 9a739c5..1d85fc6 100644
--- a/helm-charts/gerrit/templates/gerrit.storage.yaml
+++ b/helm-charts/gerrit/templates/gerrit.storage.yaml
@@ -1,4 +1,4 @@
-{{- if and .Values.gerrit.plugins.cache.enabled .Values.gerrit.plugins.downloaded }}
+{{- if and .Values.gerrit.pluginManagement.cache.enabled }}
 kind: PersistentVolumeClaim
 apiVersion: v1
 metadata:
@@ -17,7 +17,7 @@
   - ReadWriteMany
   resources:
     requests:
-      storage: {{ .Values.gerrit.plugins.cache.size }}
+      storage: {{ .Values.gerrit.pluginManagement.cache.size }}
   storageClassName: {{ .Values.storageClasses.shared.name }}
 {{- end }}
 {{ if eq .Values.gerrit.index.type "elasticsearch" -}}
diff --git a/helm-charts/gerrit/values.yaml b/helm-charts/gerrit/values.yaml
index bf17971..1135aa9 100644
--- a/helm-charts/gerrit/values.yaml
+++ b/helm-charts/gerrit/values.yaml
@@ -227,19 +227,25 @@
     # Either `lucene` or `elasticsearch`
     type: lucene
 
-  plugins:
-    packaged:
-    - commit-message-length-validator
-    - download-commands
-    - replication
-    - reviewnotes
-    downloaded:
+  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: []
+    #   installAsLibrary: false
+
     # Only downloaded plugins will be cached. This will be ignored, if no plugins
     # are downloaded.
+    libs: []
     cache:
       enabled: false
       size: 1Gi
diff --git a/operator/k8s/cluster.sample.yaml b/operator/k8s/cluster.sample.yaml
index 5db63d0..2311822 100644
--- a/operator/k8s/cluster.sample.yaml
+++ b/operator/k8s/cluster.sample.yaml
@@ -23,6 +23,7 @@
       site:
         size: 5Gi
       plugins: []
+      serverId: gerrit
       configFiles:
         gerrit.config: |-
           [auth]
@@ -30,8 +31,6 @@
           [container]
             javaOptions = -Xms200m
             javaOptions = -Xmx4g
-          [gerrit]
-            serverId = gerrit-1
           [transfer]
             timeout = 120 s
           [user]
diff --git a/operator/k8s/operator/operator.yaml b/operator/k8s/operator/operator.yaml
index 09bbb60..02c6ef6 100644
--- a/operator/k8s/operator/operator.yaml
+++ b/operator/k8s/operator/operator.yaml
@@ -36,6 +36,8 @@
           valueFrom:
             fieldRef:
               fieldPath: metadata.namespace
+        - name: INGRESS
+          value: none
         ports:
         - containerPort: 80
         readinessProbe:
diff --git a/operator/k8s/resources/rbac.yaml b/operator/k8s/resources/rbac.yaml
new file mode 100644
index 0000000..a31f4a7
--- /dev/null
+++ b/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/operator/pom.xml b/operator/pom.xml
index e73fcf2..d8d01c9 100644
--- a/operator/pom.xml
+++ b/operator/pom.xml
@@ -227,6 +227,17 @@
 			<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>
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/EnvModule.java b/operator/src/main/java/com/google/gerrit/k8s/operator/EnvModule.java
index c9d3739..1f5b04c 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/EnvModule.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/EnvModule.java
@@ -14,6 +14,7 @@
 
 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;
 
@@ -23,5 +24,12 @@
     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/operator/src/main/java/com/google/gerrit/k8s/operator/OperatorModule.java b/operator/src/main/java/com/google/gerrit/k8s/operator/OperatorModule.java
index ef8a9de..2a1b4b9 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/OperatorModule.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/OperatorModule.java
@@ -18,6 +18,7 @@
 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;
@@ -44,6 +45,7 @@
     reconcilers.addBinding().to(GerritReconciler.class);
     reconcilers.addBinding().to(GitGarbageCollectionReconciler.class);
     reconcilers.addBinding().to(ReceiverReconciler.class);
+    reconcilers.addBinding().toProvider(GerritNetworkReconcilerProvider.class);
   }
 
   private KubernetesClient getKubernetesClient() {
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/GerritClusterReconciler.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/GerritClusterReconciler.java
index 94cd59b..bd3dae3 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/GerritClusterReconciler.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/GerritClusterReconciler.java
@@ -15,42 +15,28 @@
 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.ISTIO_DESTINATION_RULE_EVENT_SOURCE;
-import static com.google.gerrit.k8s.operator.cluster.GerritClusterReconciler.ISTIO_VIRTUAL_SERVICE_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.GerritClusterIngress;
-import com.google.gerrit.k8s.operator.cluster.dependent.GerritClusterIngressCondition;
-import com.google.gerrit.k8s.operator.cluster.dependent.GerritClusterIstioCondition;
-import com.google.gerrit.k8s.operator.cluster.dependent.GerritClusterIstioGateway;
-import com.google.gerrit.k8s.operator.cluster.dependent.GerritIstioCondition;
-import com.google.gerrit.k8s.operator.cluster.dependent.GerritIstioDestinationRule;
-import com.google.gerrit.k8s.operator.cluster.dependent.GerritIstioSshCondition;
-import com.google.gerrit.k8s.operator.cluster.dependent.GerritIstioVirtualService;
-import com.google.gerrit.k8s.operator.cluster.dependent.GerritIstioVirtualServiceSSH;
-import com.google.gerrit.k8s.operator.cluster.dependent.GerritLogsPVC;
-import com.google.gerrit.k8s.operator.cluster.dependent.GitRepositoriesPVC;
 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.PluginCacheCondition;
-import com.google.gerrit.k8s.operator.cluster.dependent.PluginCachePVC;
-import com.google.gerrit.k8s.operator.cluster.dependent.ReceiverIstioCondition;
-import com.google.gerrit.k8s.operator.cluster.dependent.ReceiverIstioVirtualService;
+import com.google.gerrit.k8s.operator.cluster.dependent.SharedPVC;
 import com.google.gerrit.k8s.operator.cluster.model.GerritCluster;
 import com.google.gerrit.k8s.operator.cluster.model.GerritClusterStatus;
 import com.google.gerrit.k8s.operator.gerrit.model.Gerrit;
 import com.google.gerrit.k8s.operator.gerrit.model.GerritTemplate;
+import com.google.gerrit.k8s.operator.network.model.GerritNetwork;
 import com.google.gerrit.k8s.operator.receiver.model.Receiver;
 import com.google.gerrit.k8s.operator.receiver.model.ReceiverTemplate;
 import com.google.inject.Singleton;
-import io.fabric8.istio.api.networking.v1beta1.DestinationRule;
-import io.fabric8.istio.api.networking.v1beta1.VirtualService;
 import io.fabric8.kubernetes.api.model.ConfigMap;
 import io.fabric8.kubernetes.api.model.PersistentVolumeClaim;
 import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
@@ -72,65 +58,27 @@
 @ControllerConfiguration(
     dependents = {
       @Dependent(
-          name = "git-repositories-pvc",
-          type = GitRepositoriesPVC.class,
-          useEventSourceWithName = PVC_EVENT_SOURCE),
-      @Dependent(
-          name = "gerrit-logs-pvc",
-          type = GerritLogsPVC.class,
+          name = "shared-pvc",
+          type = SharedPVC.class,
           useEventSourceWithName = PVC_EVENT_SOURCE),
       @Dependent(
           type = NfsIdmapdConfigMap.class,
           reconcilePrecondition = NfsWorkaroundCondition.class,
           useEventSourceWithName = CM_EVENT_SOURCE),
       @Dependent(
-          type = PluginCachePVC.class,
-          reconcilePrecondition = PluginCacheCondition.class,
-          useEventSourceWithName = PVC_EVENT_SOURCE),
-      @Dependent(
           name = "gerrits",
           type = ClusterManagedGerrit.class,
           reconcilePrecondition = ClusterManagedGerritCondition.class,
-          dependsOn = {"git-repositories-pvc", "gerrit-logs-pvc"},
           useEventSourceWithName = CLUSTER_MANAGED_GERRIT_EVENT_SOURCE),
       @Dependent(
           name = "receiver",
           type = ClusterManagedReceiver.class,
           reconcilePrecondition = ClusterManagedReceiverCondition.class,
-          dependsOn = {"git-repositories-pvc", "gerrit-logs-pvc"},
           useEventSourceWithName = CLUSTER_MANAGED_RECEIVER_EVENT_SOURCE),
       @Dependent(
-          name = "gerrit-destination-rules",
-          type = GerritIstioDestinationRule.class,
-          reconcilePrecondition = GerritIstioCondition.class,
-          dependsOn = {"gerrits"},
-          useEventSourceWithName = ISTIO_DESTINATION_RULE_EVENT_SOURCE),
-      @Dependent(
-          name = "gerrit-istio-gateway",
-          type = GerritClusterIstioGateway.class,
-          reconcilePrecondition = GerritClusterIstioCondition.class),
-      @Dependent(
-          name = "gerrit-istio-virtual-service",
-          type = GerritIstioVirtualService.class,
-          reconcilePrecondition = GerritIstioCondition.class,
-          dependsOn = {"gerrit-istio-gateway", "gerrits"},
-          useEventSourceWithName = ISTIO_VIRTUAL_SERVICE_EVENT_SOURCE),
-      @Dependent(
-          name = "gerrit-istio-virtual-service-ssh",
-          type = GerritIstioVirtualServiceSSH.class,
-          reconcilePrecondition = GerritIstioSshCondition.class,
-          dependsOn = {"gerrit-istio-gateway", "gerrits"},
-          useEventSourceWithName = ISTIO_VIRTUAL_SERVICE_EVENT_SOURCE),
-      @Dependent(
-          name = "receiver-istio-virtual-service",
-          type = ReceiverIstioVirtualService.class,
-          reconcilePrecondition = ReceiverIstioCondition.class,
-          dependsOn = {"gerrit-istio-gateway", "receiver"},
-          useEventSourceWithName = ISTIO_VIRTUAL_SERVICE_EVENT_SOURCE),
-      @Dependent(
-          name = "gerrit-ingress",
-          type = GerritClusterIngress.class,
-          reconcilePrecondition = GerritClusterIngressCondition.class),
+          type = ClusterManagedGerritNetwork.class,
+          reconcilePrecondition = ClusterManagedGerritNetworkCondition.class,
+          useEventSourceWithName = CLUSTER_MANAGED_GERRIT_NETWORK_EVENT_SOURCE),
     })
 public class GerritClusterReconciler
     implements Reconciler<GerritCluster>, EventSourceInitializer<GerritCluster> {
@@ -138,10 +86,8 @@
   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 ISTIO_DESTINATION_RULE_EVENT_SOURCE =
-      "gerrit-cluster-istio-destination-rule";
-  public static final String ISTIO_VIRTUAL_SERVICE_EVENT_SOURCE =
-      "gerrit-cluster-istio-virtual-service";
+  public static final String CLUSTER_MANAGED_GERRIT_NETWORK_EVENT_SOURCE =
+      "cluster-managed-gerrit-network";
 
   @Override
   public Map<String, EventSource> prepareEventSources(EventSourceContext<GerritCluster> context) {
@@ -153,10 +99,6 @@
         new InformerEventSource<>(
             InformerConfiguration.from(PersistentVolumeClaim.class, context).build(), context);
 
-    InformerEventSource<VirtualService, GerritCluster> virtualServiceEventSource =
-        new InformerEventSource<>(
-            InformerConfiguration.from(VirtualService.class, context).build(), context);
-
     InformerEventSource<Gerrit, GerritCluster> clusterManagedGerritEventSource =
         new InformerEventSource<>(
             InformerConfiguration.from(Gerrit.class, context).build(), context);
@@ -165,17 +107,17 @@
         new InformerEventSource<>(
             InformerConfiguration.from(Receiver.class, context).build(), context);
 
-    InformerEventSource<DestinationRule, GerritCluster> gerritIstioDestinationRuleEventSource =
+    InformerEventSource<GerritNetwork, GerritCluster> clusterManagedGerritNetworkEventSource =
         new InformerEventSource<>(
-            InformerConfiguration.from(DestinationRule.class, context).build(), context);
+            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(ISTIO_DESTINATION_RULE_EVENT_SOURCE, gerritIstioDestinationRuleEventSource);
-    eventSources.put(ISTIO_VIRTUAL_SERVICE_EVENT_SOURCE, virtualServiceEventSource);
+    eventSources.put(
+        CLUSTER_MANAGED_GERRIT_NETWORK_EVENT_SOURCE, clusterManagedGerritNetworkEventSource);
     return eventSources;
   }
 
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerritNetwork.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerritNetwork.java
new file mode 100644
index 0000000..62790f0
--- /dev/null
+++ b/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.cluster.model.GerritCluster;
+import com.google.gerrit.k8s.operator.gerrit.model.GerritTemplate;
+import com.google.gerrit.k8s.operator.gerrit.model.GerritTemplateSpec.GerritMode;
+import com.google.gerrit.k8s.operator.network.model.GerritNetwork;
+import com.google.gerrit.k8s.operator.network.model.GerritNetworkSpec;
+import com.google.gerrit.k8s.operator.network.model.NetworkMember;
+import com.google.gerrit.k8s.operator.network.model.NetworkMemberWithSsh;
+import com.google.gerrit.k8s.operator.receiver.model.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/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/PluginCacheCondition.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerritNetworkCondition.java
similarity index 72%
rename from operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/PluginCacheCondition.java
rename to operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerritNetworkCondition.java
index 0e4f160..c659f6f 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/PluginCacheCondition.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerritNetworkCondition.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2022 The Android Open Source Project
+// 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.
@@ -15,18 +15,19 @@
 package com.google.gerrit.k8s.operator.cluster.dependent;
 
 import com.google.gerrit.k8s.operator.cluster.model.GerritCluster;
-import io.fabric8.kubernetes.api.model.PersistentVolumeClaim;
+import com.google.gerrit.k8s.operator.network.model.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 PluginCacheCondition implements Condition<PersistentVolumeClaim, GerritCluster> {
+public class ClusterManagedGerritNetworkCondition
+    implements Condition<GerritNetwork, GerritCluster> {
 
   @Override
   public boolean isMet(
-      DependentResource<PersistentVolumeClaim, GerritCluster> dependentResource,
+      DependentResource<GerritNetwork, GerritCluster> dependentResource,
       GerritCluster gerritCluster,
       Context<GerritCluster> context) {
-    return gerritCluster.getSpec().getStorage().getPluginCacheStorage().isEnabled();
+    return gerritCluster.getSpec().getIngress().isEnabled();
   }
 }
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritClusterIngress.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritClusterIngress.java
deleted file mode 100644
index 4940021..0000000
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritClusterIngress.java
+++ /dev/null
@@ -1,201 +0,0 @@
-// 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.common.collect.ArrayListMultimap;
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.k8s.operator.cluster.model.GerritCluster;
-import com.google.gerrit.k8s.operator.gerrit.dependent.GerritService;
-import com.google.gerrit.k8s.operator.gerrit.model.Gerrit;
-import com.google.gerrit.k8s.operator.gerrit.model.GerritTemplate;
-import com.google.gerrit.k8s.operator.gerrit.model.GerritTemplateSpec.GerritMode;
-import com.google.gerrit.k8s.operator.receiver.dependent.ReceiverService;
-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.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.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-@KubernetesDependent
-public class GerritClusterIngress extends CRUDKubernetesDependentResource<Ingress, GerritCluster> {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-  private static final String UPLOAD_PACK_URL_PATTERN = "/.*/git-upload-pack";
-  public static final String INGRESS_NAME = "gerrit-ingress";
-
-  public GerritClusterIngress() {
-    super(Ingress.class);
-  }
-
-  @Override
-  protected Ingress desired(GerritCluster gerritCluster, Context<GerritCluster> context) {
-    Ingress gerritIngress =
-        new IngressBuilder()
-            .withNewMetadata()
-            .withName("gerrit-ingress")
-            .withNamespace(gerritCluster.getMetadata().getNamespace())
-            .withLabels(gerritCluster.getLabels("gerrit-ingress", this.getClass().getSimpleName()))
-            .withAnnotations(getAnnotations(gerritCluster))
-            .endMetadata()
-            .withNewSpec()
-            .withTls(getIngressTLS(gerritCluster))
-            .withRules(getIngressRule(gerritCluster))
-            .endSpec()
-            .build();
-
-    return gerritIngress;
-  }
-
-  private Map<String, String> getAnnotations(GerritCluster gerritCluster) {
-    Map<String, String> annotations = gerritCluster.getSpec().getIngress().getAnnotations();
-    annotations.put("nginx.ingress.kubernetes.io/use-regex", "true");
-    annotations.put("kubernetes.io/ingress.class", "nginx");
-
-    Optional<GerritTemplate> gerritReplica =
-        gerritCluster.getSpec().getGerrits().stream()
-            .filter(g -> g.getSpec().getMode().equals(GerritMode.REPLICA))
-            .findFirst();
-    if (gerritReplica.isPresent()) {
-      String svcName = GerritService.getName(gerritReplica.get());
-      StringBuilder configSnippet = new StringBuilder();
-      configSnippet.append("if ($args ~ service=git-upload-pack){");
-      configSnippet.append("\n");
-      configSnippet.append("  set $proxy_upstream_name \"");
-      configSnippet.append(gerritCluster.getMetadata().getNamespace());
-      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}");
-      annotations.put(
-          "nginx.ingress.kubernetes.io/configuration-snippet", configSnippet.toString());
-    }
-    return annotations;
-  }
-
-  private IngressTLS getIngressTLS(GerritCluster gerritCluster) {
-    if (gerritCluster.getSpec().getIngress().getTls().isEnabled()) {
-      return new IngressTLSBuilder()
-          .withHosts(gerritCluster.getSpec().getIngress().getHost())
-          .withSecretName(gerritCluster.getSpec().getIngress().getTls().getSecret())
-          .build();
-    }
-    return new IngressTLS();
-  }
-
-  private IngressRule getIngressRule(GerritCluster gerritCluster) {
-    List<HTTPIngressPath> ingressPaths = getGerritHTTPIngressPaths(gerritCluster);
-    ingressPaths.addAll(getReceiverIngressPaths(gerritCluster));
-
-    return new IngressRuleBuilder()
-        .withHost(gerritCluster.getSpec().getIngress().getHost())
-        .withNewHttp()
-        .withPaths(ingressPaths)
-        .endHttp()
-        .build();
-  }
-
-  private List<HTTPIngressPath> getGerritHTTPIngressPaths(GerritCluster gerritCluster) {
-    ServiceBackendPort port =
-        new ServiceBackendPortBuilder().withName(GerritService.HTTP_PORT_NAME).build();
-
-    ArrayListMultimap<GerritMode, HTTPIngressPath> pathsByMode = ArrayListMultimap.create();
-    List<Gerrit> gerrits =
-        gerritCluster.getSpec().getGerrits().stream()
-            .map(g -> g.toGerrit(gerritCluster))
-            .collect(Collectors.toList());
-    for (Gerrit gerrit : gerrits) {
-      switch (gerrit.getSpec().getMode()) {
-        case REPLICA:
-          pathsByMode.put(
-              GerritMode.REPLICA,
-              new HTTPIngressPathBuilder()
-                  .withPathType("Prefix")
-                  .withPath(UPLOAD_PACK_URL_PATTERN)
-                  .withNewBackend()
-                  .withNewService()
-                  .withName(GerritService.getName(gerrit))
-                  .withPort(port)
-                  .endService()
-                  .endBackend()
-                  .build());
-          break;
-        case PRIMARY:
-          pathsByMode.put(
-              GerritMode.PRIMARY,
-              new HTTPIngressPathBuilder()
-                  .withPathType("Prefix")
-                  .withPath("/")
-                  .withNewBackend()
-                  .withNewService()
-                  .withName(GerritService.getName(gerrit))
-                  .withPort(port)
-                  .endService()
-                  .endBackend()
-                  .build());
-          break;
-        default:
-          logger.atFine().log(
-              "Encountered unknown Gerrit mode when reconciling Ingress: %s",
-              gerrit.getSpec().getMode());
-      }
-    }
-
-    List<HTTPIngressPath> paths = new ArrayList<>();
-    paths.addAll(pathsByMode.get(GerritMode.REPLICA));
-    paths.addAll(pathsByMode.get(GerritMode.PRIMARY));
-    return paths;
-  }
-
-  private List<HTTPIngressPath> getReceiverIngressPaths(GerritCluster gerritCluster) {
-    String svcName = ReceiverService.getName(gerritCluster.getSpec().getReceiver());
-    List<HTTPIngressPath> paths = new ArrayList<>();
-    ServiceBackendPort port =
-        new ServiceBackendPortBuilder().withName(ReceiverService.HTTP_PORT_NAME).build();
-
-    for (String path : Set.of("/a/projects", "/new", "/git")) {
-      paths.add(
-          new HTTPIngressPathBuilder()
-              .withPathType("Prefix")
-              .withPath(path)
-              .withNewBackend()
-              .withNewService()
-              .withName(svcName)
-              .withPort(port)
-              .endService()
-              .endBackend()
-              .build());
-    }
-    return paths;
-  }
-}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritClusterIngressCondition.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritClusterIngressCondition.java
deleted file mode 100644
index 9b0d647..0000000
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritClusterIngressCondition.java
+++ /dev/null
@@ -1,36 +0,0 @@
-// 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.cluster.model.GerritCluster;
-import com.google.gerrit.k8s.operator.cluster.model.GerritClusterIngressConfig.IngressType;
-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 GerritClusterIngressCondition implements Condition<VirtualService, GerritCluster> {
-
-  @Override
-  public boolean isMet(
-      DependentResource<VirtualService, GerritCluster> dependentResource,
-      GerritCluster gerritCluster,
-      Context<GerritCluster> context) {
-    return gerritCluster.getSpec().getIngress().isEnabled()
-        && gerritCluster.getSpec().getIngress().getType() == IngressType.INGRESS
-        && (!gerritCluster.getSpec().getGerrits().isEmpty()
-            || gerritCluster.getSpec().getReceiver() != null);
-  }
-}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritClusterIstioCondition.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritClusterIstioCondition.java
deleted file mode 100644
index 302c9e7..0000000
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritClusterIstioCondition.java
+++ /dev/null
@@ -1,36 +0,0 @@
-// 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.cluster.model.GerritCluster;
-import com.google.gerrit.k8s.operator.cluster.model.GerritClusterIngressConfig.IngressType;
-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 GerritClusterIstioCondition implements Condition<VirtualService, GerritCluster> {
-
-  @Override
-  public boolean isMet(
-      DependentResource<VirtualService, GerritCluster> dependentResource,
-      GerritCluster gerritCluster,
-      Context<GerritCluster> context) {
-    return gerritCluster.getSpec().getIngress().isEnabled()
-        && gerritCluster.getSpec().getIngress().getType() == IngressType.ISTIO
-        && (!gerritCluster.getSpec().getGerrits().isEmpty()
-            || gerritCluster.getSpec().getReceiver() != null);
-  }
-}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritClusterIstioGateway.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritClusterIstioGateway.java
deleted file mode 100644
index 2003353..0000000
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritClusterIstioGateway.java
+++ /dev/null
@@ -1,117 +0,0 @@
-// 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.cluster.model.GerritCluster;
-import com.google.gerrit.k8s.operator.gerrit.model.Gerrit;
-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, GerritCluster> {
-  public static final String NAME = "gerrit-istio-gateway";
-
-  public GerritClusterIstioGateway() {
-    super(Gateway.class);
-  }
-
-  @Override
-  protected Gateway desired(GerritCluster gerritCluster, Context<GerritCluster> context) {
-    return new GatewayBuilder()
-        .withNewMetadata()
-        .withName(NAME)
-        .withNamespace(gerritCluster.getMetadata().getNamespace())
-        .withLabels(gerritCluster.getLabels(NAME, this.getClass().getSimpleName()))
-        .endMetadata()
-        .withNewSpec()
-        .withSelector(Map.of("istio", "ingressgateway"))
-        .withServers(configureServers(gerritCluster))
-        .endSpec()
-        .build();
-  }
-
-  private List<Server> configureServers(GerritCluster gerritCluster) {
-    List<Server> servers = new ArrayList<>();
-    String gerritClusterHost = gerritCluster.getSpec().getIngress().getHost();
-    List<String> httpHostnames = new ArrayList<>();
-
-    if (!gerritCluster.getSpec().getGerrits().isEmpty()) {
-      httpHostnames.add(gerritClusterHost);
-    }
-
-    if (gerritCluster.getSpec().getReceiver() != null) {
-      httpHostnames.add(ReceiverIstioVirtualService.getHostname(gerritCluster));
-    }
-
-    servers.add(
-        new ServerBuilder()
-            .withNewPort()
-            .withName("http")
-            .withNumber(80)
-            .withProtocol("HTTP")
-            .endPort()
-            .withHosts(httpHostnames)
-            .withNewTls()
-            .withHttpsRedirect(gerritCluster.getSpec().getIngress().getTls().isEnabled())
-            .endTls()
-            .build());
-
-    if (gerritCluster.getSpec().getIngress().getTls().isEnabled()) {
-      servers.add(
-          new ServerBuilder()
-              .withNewPort()
-              .withName("https")
-              .withNumber(443)
-              .withProtocol("HTTPS")
-              .endPort()
-              .withHosts(httpHostnames)
-              .withNewTls()
-              .withMode(ServerTLSSettingsTLSmode.SIMPLE)
-              .withCredentialName(gerritCluster.getSpec().getIngress().getTls().getSecret())
-              .endTls()
-              .build());
-    }
-
-    List<Gerrit> gerrits = client.resources(Gerrit.class).list().getItems();
-    List<String> sshHostnames = new ArrayList<>();
-    if (!gerrits.isEmpty()) {
-      sshHostnames.add(gerritClusterHost);
-      for (Gerrit gerrit : gerrits) {
-        if (gerrit.getSpec().getService().isSshEnabled()) {
-          sshHostnames.add(gerrit.getMetadata().getName() + "." + gerritClusterHost);
-        }
-      }
-      servers.add(
-          new ServerBuilder()
-              .withNewPort()
-              .withName("ssh")
-              .withNumber(29418)
-              .withProtocol("TCP")
-              .endPort()
-              .withHosts(sshHostnames)
-              .build());
-    }
-
-    return servers;
-  }
-}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioDestinationRule.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioDestinationRule.java
deleted file mode 100644
index 87a8aee..0000000
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioDestinationRule.java
+++ /dev/null
@@ -1,93 +0,0 @@
-// 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.cluster.model.GerritCluster;
-import com.google.gerrit.k8s.operator.gerrit.dependent.GerritService;
-import com.google.gerrit.k8s.operator.gerrit.model.GerritTemplate;
-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.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, GerritCluster>
-    implements Creator<DestinationRule, GerritCluster>,
-        Updater<DestinationRule, GerritCluster>,
-        Deleter<GerritCluster>,
-        BulkDependentResource<DestinationRule, GerritCluster>,
-        GarbageCollected<GerritCluster> {
-
-  public GerritIstioDestinationRule() {
-    super(DestinationRule.class);
-  }
-
-  protected DestinationRule desired(GerritCluster gerritCluster, GerritTemplate gerrit) {
-
-    return new DestinationRuleBuilder()
-        .withNewMetadata()
-        .withName(getName(gerrit))
-        .withNamespace(gerritCluster.getMetadata().getNamespace())
-        .withLabels(gerritCluster.getLabels(getName(gerrit), this.getClass().getSimpleName()))
-        .endMetadata()
-        .withNewSpec()
-        .withHost(GerritService.getHostname(gerrit.toGerrit(gerritCluster)))
-        .withTrafficPolicy(
-            new TrafficPolicyBuilder()
-                .withNewLoadBalancer()
-                .withNewLoadBalancerSettingsSimpleLbPolicy()
-                .withSimple(LoadBalancerSettingsSimpleLB.LEAST_CONN)
-                .endLoadBalancerSettingsSimpleLbPolicy()
-                .endLoadBalancer()
-                .build())
-        .endSpec()
-        .build();
-  }
-
-  public static String getName(GerritTemplate gerrit) {
-    return gerrit.getMetadata().getName();
-  }
-
-  @Override
-  public Map<String, DestinationRule> desiredResources(
-      GerritCluster gerritCluster, Context<GerritCluster> context) {
-    Map<String, DestinationRule> drs = new HashMap<>();
-    for (GerritTemplate template : gerritCluster.getSpec().getGerrits()) {
-      drs.put(template.getMetadata().getName(), desired(gerritCluster, template));
-    }
-    return drs;
-  }
-
-  @Override
-  public Map<String, DestinationRule> getSecondaryResources(
-      GerritCluster gerritCluster, Context<GerritCluster> 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/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioSshCondition.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioSshCondition.java
deleted file mode 100644
index 2d45838..0000000
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioSshCondition.java
+++ /dev/null
@@ -1,36 +0,0 @@
-// 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.cluster.model.GerritCluster;
-import com.google.gerrit.k8s.operator.cluster.model.GerritClusterIngressConfig.IngressType;
-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 GerritIstioSshCondition implements Condition<VirtualService, GerritCluster> {
-
-  @Override
-  public boolean isMet(
-      DependentResource<VirtualService, GerritCluster> dependentResource,
-      GerritCluster gerritCluster,
-      Context<GerritCluster> context) {
-    return gerritCluster.getSpec().getIngress().isEnabled()
-        && gerritCluster.getSpec().getIngress().getType() == IngressType.ISTIO
-        && gerritCluster.getSpec().getGerrits().stream()
-            .anyMatch(g -> g.getSpec().getService().isSshEnabled());
-  }
-}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioVirtualService.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioVirtualService.java
deleted file mode 100644
index 2495925..0000000
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioVirtualService.java
+++ /dev/null
@@ -1,149 +0,0 @@
-// 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.common.collect.ArrayListMultimap;
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.k8s.operator.cluster.model.GerritCluster;
-import com.google.gerrit.k8s.operator.gerrit.dependent.GerritService;
-import com.google.gerrit.k8s.operator.gerrit.model.GerritTemplate;
-import com.google.gerrit.k8s.operator.gerrit.model.GerritTemplateSpec.GerritMode;
-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.StringMatchBuilder;
-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(resourceDiscriminator = GerritIstioVirtualServiceDiscriminator.class)
-public class GerritIstioVirtualService
-    extends CRUDKubernetesDependentResource<VirtualService, GerritCluster> {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-  private static final String UPLOAD_PACK_INFO_REF_URL_PATTERN = "^/(.*)/info/refs$";
-  private static final String UPLOAD_PACK_URL_PATTERN = "^/(.*)/git-upload-pack$";
-  public static final String NAME_SUFFIX = "gerrit-http-virtual-service";
-
-  public GerritIstioVirtualService() {
-    super(VirtualService.class);
-  }
-
-  @Override
-  protected VirtualService desired(GerritCluster gerritCluster, Context<GerritCluster> context) {
-    String gerritClusterHost = gerritCluster.getSpec().getIngress().getHost();
-
-    return new VirtualServiceBuilder()
-        .withNewMetadata()
-        .withName(gerritCluster.getDependentResourceName(NAME_SUFFIX))
-        .withNamespace(gerritCluster.getMetadata().getNamespace())
-        .withLabels(
-            gerritCluster.getLabels(
-                gerritCluster.getDependentResourceName(NAME_SUFFIX),
-                this.getClass().getSimpleName()))
-        .endMetadata()
-        .withNewSpec()
-        .withHosts(gerritClusterHost)
-        .withGateways(GerritClusterIstioGateway.NAME)
-        .withHttp(getHTTPRoutes(gerritCluster))
-        .endSpec()
-        .build();
-  }
-
-  private List<HTTPRoute> getHTTPRoutes(GerritCluster gerritCluster) {
-    List<GerritTemplate> gerrits = gerritCluster.getSpec().getGerrits();
-    ArrayListMultimap<GerritMode, HTTPRoute> routesByMode = ArrayListMultimap.create();
-    for (GerritTemplate gerrit : gerrits) {
-      switch (gerrit.getSpec().getMode()) {
-        case REPLICA:
-          routesByMode.put(
-              GerritMode.REPLICA,
-              new HTTPRouteBuilder()
-                  .withName("gerrit-replica-" + gerrit.getMetadata().getName())
-                  .withMatch(
-                      new HTTPMatchRequestBuilder()
-                          .withNewUri()
-                          .withNewStringMatchRegexType()
-                          .withRegex(UPLOAD_PACK_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(),
-                      new HTTPMatchRequestBuilder()
-                          .withNewUri()
-                          .withNewStringMatchRegexType()
-                          .withRegex(UPLOAD_PACK_URL_PATTERN)
-                          .endStringMatchRegexType()
-                          .endUri()
-                          .withIgnoreUriCase()
-                          .withNewMethod()
-                          .withNewStringMatchExactType()
-                          .withExact("POST")
-                          .endStringMatchExactType()
-                          .endMethod()
-                          .build())
-                  .withRoute(getGerritHTTPDestinations(gerrit, gerritCluster))
-                  .build());
-          break;
-        case PRIMARY:
-          routesByMode.put(
-              GerritMode.PRIMARY,
-              new HTTPRouteBuilder()
-                  .withName("gerrit-primary-" + gerrit.getMetadata().getName())
-                  .withRoute(getGerritHTTPDestinations(gerrit, gerritCluster))
-                  .build());
-          break;
-        default:
-          logger.atFine().log(
-              "Encountered unknown Gerrit mode when reconciling VirtualSErvice: %s",
-              gerrit.getSpec().getMode());
-      }
-    }
-
-    List<HTTPRoute> routes = new ArrayList<>();
-    routes.addAll(routesByMode.get(GerritMode.REPLICA));
-    routes.addAll(routesByMode.get(GerritMode.PRIMARY));
-    return routes;
-  }
-
-  private HTTPRouteDestination getGerritHTTPDestinations(
-      GerritTemplate gerrit, GerritCluster gerritCluster) {
-    return new HTTPRouteDestinationBuilder()
-        .withNewDestination()
-        .withHost(GerritService.getHostname(gerrit.toGerrit(gerritCluster)))
-        .withNewPort()
-        .withNumber(gerrit.getSpec().getService().getHttpPort())
-        .endPort()
-        .endDestination()
-        .build();
-  }
-}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioVirtualServiceDiscriminator.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioVirtualServiceDiscriminator.java
deleted file mode 100644
index 9405379..0000000
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioVirtualServiceDiscriminator.java
+++ /dev/null
@@ -1,44 +0,0 @@
-// 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.ISTIO_VIRTUAL_SERVICE_EVENT_SOURCE;
-
-import com.google.gerrit.k8s.operator.cluster.model.GerritCluster;
-import io.fabric8.istio.api.networking.v1beta1.VirtualService;
-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 GerritIstioVirtualServiceDiscriminator
-    implements ResourceDiscriminator<VirtualService, GerritCluster> {
-  @Override
-  public Optional<VirtualService> distinguish(
-      Class<VirtualService> resource, GerritCluster gerritCluster, Context<GerritCluster> context) {
-    InformerEventSource<VirtualService, GerritCluster> ies =
-        (InformerEventSource<VirtualService, GerritCluster>)
-            context
-                .eventSourceRetriever()
-                .getResourceEventSourceFor(
-                    VirtualService.class, ISTIO_VIRTUAL_SERVICE_EVENT_SOURCE);
-
-    return ies.get(
-        new ResourceID(
-            gerritCluster.getDependentResourceName(GerritIstioVirtualService.NAME_SUFFIX),
-            gerritCluster.getMetadata().getNamespace()));
-  }
-}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioVirtualServiceSSH.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioVirtualServiceSSH.java
deleted file mode 100644
index 746f705..0000000
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioVirtualServiceSSH.java
+++ /dev/null
@@ -1,96 +0,0 @@
-// 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.cluster.model.GerritCluster;
-import com.google.gerrit.k8s.operator.gerrit.dependent.GerritService;
-import com.google.gerrit.k8s.operator.gerrit.model.GerritTemplate;
-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.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.stream.Collectors;
-
-@KubernetesDependent(resourceDiscriminator = GerritIstioVirtualServiceSSHDiscriminator.class)
-public class GerritIstioVirtualServiceSSH
-    extends CRUDKubernetesDependentResource<VirtualService, GerritCluster> {
-  public static final String NAME_SUFFIX = "gerrit-ssh-virtual-service";
-
-  public GerritIstioVirtualServiceSSH() {
-    super(VirtualService.class);
-  }
-
-  @Override
-  protected VirtualService desired(GerritCluster gerritCluster, Context<GerritCluster> context) {
-    String gerritClusterHost = gerritCluster.getSpec().getIngress().getHost();
-    List<GerritTemplate> gerrits = gerritCluster.getSpec().getGerrits();
-
-    return new VirtualServiceBuilder()
-        .withNewMetadata()
-        .withName(gerritCluster.getDependentResourceName(NAME_SUFFIX))
-        .withNamespace(gerritCluster.getMetadata().getNamespace())
-        .withLabels(
-            gerritCluster.getLabels(
-                gerritCluster.getDependentResourceName(NAME_SUFFIX),
-                this.getClass().getSimpleName()))
-        .endMetadata()
-        .withNewSpec()
-        .withHosts(collectHosts(gerrits, gerritClusterHost))
-        .withGateways(GerritClusterIstioGateway.NAME)
-        .withTcp(getTCPRoutes(gerrits, gerritCluster))
-        .endSpec()
-        .build();
-  }
-
-  private List<String> collectHosts(List<GerritTemplate> gerrits, String gerritClusterHost) {
-    return gerrits.stream()
-        .map(g -> g.getMetadata().getName() + "." + gerritClusterHost)
-        .collect(Collectors.toList());
-  }
-
-  private List<TCPRoute> getTCPRoutes(List<GerritTemplate> gerrits, GerritCluster gerritCluster) {
-    List<TCPRoute> routes = new ArrayList<>();
-    for (GerritTemplate gerrit : gerrits) {
-      if (gerrit.getSpec().getService().isSshEnabled()) {
-        routes.add(
-            new TCPRouteBuilder()
-                .withMatch(List.of(new L4MatchAttributesBuilder().withPort(29418).build()))
-                .withRoute(getGerritTCPDestination(gerrit, gerritCluster))
-                .build());
-      }
-    }
-    return routes;
-  }
-
-  private RouteDestination getGerritTCPDestination(
-      GerritTemplate gerrit, GerritCluster gerritCluster) {
-    return new RouteDestinationBuilder()
-        .withNewDestination()
-        .withHost(GerritService.getHostname(gerrit.toGerrit(gerritCluster)))
-        .withNewPort()
-        .withNumber(gerrit.getSpec().getService().getSshPort())
-        .endPort()
-        .endDestination()
-        .build();
-  }
-}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioVirtualServiceSSHDiscriminator.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioVirtualServiceSSHDiscriminator.java
deleted file mode 100644
index 0237125..0000000
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioVirtualServiceSSHDiscriminator.java
+++ /dev/null
@@ -1,44 +0,0 @@
-// 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.ISTIO_VIRTUAL_SERVICE_EVENT_SOURCE;
-
-import com.google.gerrit.k8s.operator.cluster.model.GerritCluster;
-import io.fabric8.istio.api.networking.v1beta1.VirtualService;
-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 GerritIstioVirtualServiceSSHDiscriminator
-    implements ResourceDiscriminator<VirtualService, GerritCluster> {
-  @Override
-  public Optional<VirtualService> distinguish(
-      Class<VirtualService> resource, GerritCluster gerritCluster, Context<GerritCluster> context) {
-    InformerEventSource<VirtualService, GerritCluster> ies =
-        (InformerEventSource<VirtualService, GerritCluster>)
-            context
-                .eventSourceRetriever()
-                .getResourceEventSourceFor(
-                    VirtualService.class, ISTIO_VIRTUAL_SERVICE_EVENT_SOURCE);
-
-    return ies.get(
-        new ResourceID(
-            gerritCluster.getDependentResourceName(GerritIstioVirtualServiceSSH.NAME_SUFFIX),
-            gerritCluster.getMetadata().getNamespace()));
-  }
-}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritLogsPVC.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritLogsPVC.java
deleted file mode 100644
index c261a97..0000000
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritLogsPVC.java
+++ /dev/null
@@ -1,56 +0,0 @@
-// 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.cluster.model.GerritCluster;
-import com.google.gerrit.k8s.operator.util.CRUDKubernetesDependentPVCResource;
-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 = GerritLogsPVCDiscriminator.class)
-public class GerritLogsPVC extends CRUDKubernetesDependentPVCResource<GerritCluster> {
-
-  public static final String LOGS_PVC_NAME = "gerrit-logs-pvc";
-
-  @Override
-  protected PersistentVolumeClaim desiredPVC(
-      GerritCluster gerritCluster, Context<GerritCluster> context) {
-    PersistentVolumeClaim gerritLogsPvc =
-        new PersistentVolumeClaimBuilder()
-            .withNewMetadata()
-            .withName(LOGS_PVC_NAME)
-            .withNamespace(gerritCluster.getMetadata().getNamespace())
-            .withLabels(
-                gerritCluster.getLabels("gerrit-logs-storage", this.getClass().getSimpleName()))
-            .endMetadata()
-            .withNewSpec()
-            .withAccessModes("ReadWriteMany")
-            .withNewResources()
-            .withRequests(
-                Map.of("storage", gerritCluster.getSpec().getStorage().getLogsStorage().getSize()))
-            .endResources()
-            .withStorageClassName(
-                gerritCluster.getSpec().getStorage().getStorageClasses().getReadWriteMany())
-            .withSelector(gerritCluster.getSpec().getStorage().getLogsStorage().getSelector())
-            .withVolumeName(gerritCluster.getSpec().getStorage().getLogsStorage().getVolumeName())
-            .endSpec()
-            .build();
-
-    return gerritLogsPvc;
-  }
-}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritLogsPVCDiscriminator.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritLogsPVCDiscriminator.java
deleted file mode 100644
index fc56ad4..0000000
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritLogsPVCDiscriminator.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// 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.cluster.model.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 GerritLogsPVCDiscriminator
-    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(GerritLogsPVC.LOGS_PVC_NAME, primary.getMetadata().getNamespace()));
-  }
-}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GitRepositoriesPVC.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GitRepositoriesPVC.java
deleted file mode 100644
index 21c524d..0000000
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GitRepositoriesPVC.java
+++ /dev/null
@@ -1,59 +0,0 @@
-// 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.cluster.model.GerritCluster;
-import com.google.gerrit.k8s.operator.shared.model.GerritStorageConfig;
-import com.google.gerrit.k8s.operator.shared.model.SharedStorage;
-import com.google.gerrit.k8s.operator.util.CRUDKubernetesDependentPVCResource;
-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 = GitRepositoriesPVCDiscriminator.class)
-public class GitRepositoriesPVC extends CRUDKubernetesDependentPVCResource<GerritCluster> {
-
-  public static final String REPOSITORY_PVC_NAME = "git-repositories-pvc";
-
-  @Override
-  protected PersistentVolumeClaim desiredPVC(
-      GerritCluster gerritCluster, Context<GerritCluster> context) {
-    GerritStorageConfig storageConfig = gerritCluster.getSpec().getStorage();
-    SharedStorage gitRepoStorage = storageConfig.getGitRepositoryStorage();
-    PersistentVolumeClaim gitRepoPvc =
-        new PersistentVolumeClaimBuilder()
-            .withNewMetadata()
-            .withName(REPOSITORY_PVC_NAME)
-            .withNamespace(gerritCluster.getMetadata().getNamespace())
-            .withLabels(
-                gerritCluster.getLabels(
-                    "git-repositories-storage", this.getClass().getSimpleName()))
-            .endMetadata()
-            .withNewSpec()
-            .withAccessModes("ReadWriteMany")
-            .withNewResources()
-            .withRequests(Map.of("storage", gitRepoStorage.getSize()))
-            .endResources()
-            .withStorageClassName(storageConfig.getStorageClasses().getReadWriteMany())
-            .withSelector(gitRepoStorage.getSelector())
-            .withVolumeName(gitRepoStorage.getVolumeName())
-            .endSpec()
-            .build();
-
-    return gitRepoPvc;
-  }
-}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GitRepositoriesPVCDiscriminator.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GitRepositoriesPVCDiscriminator.java
deleted file mode 100644
index 24f7a73..0000000
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GitRepositoriesPVCDiscriminator.java
+++ /dev/null
@@ -1,44 +0,0 @@
-// 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.cluster.model.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 GitRepositoriesPVCDiscriminator
-    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(
-            GitRepositoriesPVC.REPOSITORY_PVC_NAME, primary.getMetadata().getNamespace()));
-  }
-}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/PluginCachePVC.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/PluginCachePVC.java
deleted file mode 100644
index 5cef0d7..0000000
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/PluginCachePVC.java
+++ /dev/null
@@ -1,58 +0,0 @@
-// 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.cluster.model.GerritCluster;
-import com.google.gerrit.k8s.operator.shared.model.GerritStorageConfig;
-import com.google.gerrit.k8s.operator.shared.model.SharedStorage;
-import com.google.gerrit.k8s.operator.util.CRUDKubernetesDependentPVCResource;
-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 = PluginCachePVCDiscriminator.class)
-public class PluginCachePVC extends CRUDKubernetesDependentPVCResource<GerritCluster> {
-
-  public static final String PLUGIN_CACHE_PVC_NAME = "gerrit-plugin-cache-pvc";
-
-  @Override
-  protected PersistentVolumeClaim desiredPVC(
-      GerritCluster gerritCluster, Context<GerritCluster> context) {
-    GerritStorageConfig storageConfig = gerritCluster.getSpec().getStorage();
-    SharedStorage pluginStorage = storageConfig.getGitRepositoryStorage();
-    PersistentVolumeClaim gerritPluginCachePvc =
-        new PersistentVolumeClaimBuilder()
-            .withNewMetadata()
-            .withName(PLUGIN_CACHE_PVC_NAME)
-            .withNamespace(gerritCluster.getMetadata().getNamespace())
-            .withLabels(
-                gerritCluster.getLabels(PLUGIN_CACHE_PVC_NAME, this.getClass().getSimpleName()))
-            .endMetadata()
-            .withNewSpec()
-            .withAccessModes("ReadWriteMany")
-            .withNewResources()
-            .withRequests(Map.of("storage", pluginStorage.getSize()))
-            .endResources()
-            .withStorageClassName(storageConfig.getStorageClasses().getReadWriteMany())
-            .withSelector(pluginStorage.getSelector())
-            .withVolumeName(pluginStorage.getVolumeName())
-            .endSpec()
-            .build();
-
-    return gerritPluginCachePvc;
-  }
-}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ReceiverIstioCondition.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ReceiverIstioCondition.java
deleted file mode 100644
index 96c70e4..0000000
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ReceiverIstioCondition.java
+++ /dev/null
@@ -1,35 +0,0 @@
-// 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.cluster.model.GerritCluster;
-import com.google.gerrit.k8s.operator.cluster.model.GerritClusterIngressConfig.IngressType;
-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 ReceiverIstioCondition implements Condition<VirtualService, GerritCluster> {
-
-  @Override
-  public boolean isMet(
-      DependentResource<VirtualService, GerritCluster> dependentResource,
-      GerritCluster gerritCluster,
-      Context<GerritCluster> context) {
-    return gerritCluster.getSpec().getIngress().isEnabled()
-        && gerritCluster.getSpec().getIngress().getType() == IngressType.ISTIO
-        && gerritCluster.getSpec().getReceiver() != null;
-  }
-}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ReceiverIstioVirtualService.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ReceiverIstioVirtualService.java
deleted file mode 100644
index f3dbc0c..0000000
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ReceiverIstioVirtualService.java
+++ /dev/null
@@ -1,110 +0,0 @@
-// 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.cluster.model.GerritCluster;
-import com.google.gerrit.k8s.operator.receiver.dependent.ReceiverService;
-import com.google.gerrit.k8s.operator.receiver.model.Receiver;
-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.StringMatchBuilder;
-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;
-
-@KubernetesDependent(resourceDiscriminator = ReceiverIstioVirtualServiceDiscriminator.class)
-public class ReceiverIstioVirtualService
-    extends CRUDKubernetesDependentResource<VirtualService, GerritCluster> {
-  private static final String RECEIVER_INGRESS_SUBDOMAIN = "receiver";
-  public static final String NAME_SUFFIX = "receiver-virtual-service";
-
-  public ReceiverIstioVirtualService() {
-    super(VirtualService.class);
-  }
-
-  @Override
-  protected VirtualService desired(GerritCluster gerritCluster, Context<GerritCluster> context) {
-    Receiver receiver = gerritCluster.getSpec().getReceiver().toReceiver(gerritCluster);
-
-    return new VirtualServiceBuilder()
-        .withNewMetadata()
-        .withName(gerritCluster.getDependentResourceName(NAME_SUFFIX))
-        .withNamespace(receiver.getMetadata().getNamespace())
-        .withLabels(
-            gerritCluster.getLabels(
-                gerritCluster.getDependentResourceName(NAME_SUFFIX),
-                this.getClass().getSimpleName()))
-        .endMetadata()
-        .withNewSpec()
-        .withHosts(getHostname(gerritCluster))
-        .withGateways(GerritClusterIstioGateway.NAME)
-        .withHttp(getHTTPRoute(receiver))
-        .endSpec()
-        .build();
-  }
-
-  public static String getHostname(GerritCluster gerritCluster) {
-    String gerritClusterHost = gerritCluster.getSpec().getIngress().getHost();
-    return RECEIVER_INGRESS_SUBDOMAIN + "." + gerritClusterHost;
-  }
-
-  public static String getName(GerritCluster gerritCluster) {
-    return gerritCluster.getSpec().getReceiver().getMetadata().getName();
-  }
-
-  private HTTPRoute getHTTPRoute(Receiver receiver) {
-    return new HTTPRouteBuilder()
-        .withName("receiver")
-        .withMatch(getReceiverMatches())
-        .withRoute(getReceiverHTTPDestination(receiver))
-        .build();
-  }
-
-  private HTTPRouteDestination getReceiverHTTPDestination(Receiver receiver) {
-    return new HTTPRouteDestinationBuilder()
-        .withNewDestination()
-        .withHost(ReceiverService.getHostname(receiver))
-        .withNewPort()
-        .withNumber(receiver.getSpec().getService().getHttpPort())
-        .endPort()
-        .endDestination()
-        .build();
-  }
-
-  private List<HTTPMatchRequest> getReceiverMatches() {
-    List<HTTPMatchRequest> matches = new ArrayList<>();
-    matches.add(
-        new HTTPMatchRequestBuilder()
-            .withUri(new StringMatchBuilder().withNewStringMatchPrefixType("/git/").build())
-            .build());
-    matches.add(
-        new HTTPMatchRequestBuilder()
-            .withUri(new StringMatchBuilder().withNewStringMatchPrefixType("/new/").build())
-            .build());
-    matches.add(
-        new HTTPMatchRequestBuilder()
-            .withUri(new StringMatchBuilder().withNewStringMatchPrefixType("/a/projects/").build())
-            .build());
-    return matches;
-  }
-}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ReceiverIstioVirtualServiceDiscriminator.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ReceiverIstioVirtualServiceDiscriminator.java
deleted file mode 100644
index 05007f8..0000000
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ReceiverIstioVirtualServiceDiscriminator.java
+++ /dev/null
@@ -1,44 +0,0 @@
-// 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.ISTIO_VIRTUAL_SERVICE_EVENT_SOURCE;
-
-import com.google.gerrit.k8s.operator.cluster.model.GerritCluster;
-import io.fabric8.istio.api.networking.v1beta1.VirtualService;
-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 ReceiverIstioVirtualServiceDiscriminator
-    implements ResourceDiscriminator<VirtualService, GerritCluster> {
-  @Override
-  public Optional<VirtualService> distinguish(
-      Class<VirtualService> resource, GerritCluster gerritCluster, Context<GerritCluster> context) {
-    InformerEventSource<VirtualService, GerritCluster> ies =
-        (InformerEventSource<VirtualService, GerritCluster>)
-            context
-                .eventSourceRetriever()
-                .getResourceEventSourceFor(
-                    VirtualService.class, ISTIO_VIRTUAL_SERVICE_EVENT_SOURCE);
-
-    return ies.get(
-        new ResourceID(
-            gerritCluster.getDependentResourceName(ReceiverIstioVirtualService.NAME_SUFFIX),
-            gerritCluster.getMetadata().getNamespace()));
-  }
-}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/SharedPVC.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/SharedPVC.java
new file mode 100644
index 0000000..c7a9e4f
--- /dev/null
+++ b/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.cluster.model.GerritCluster;
+import com.google.gerrit.k8s.operator.shared.model.GerritStorageConfig;
+import com.google.gerrit.k8s.operator.shared.model.SharedStorage;
+import com.google.gerrit.k8s.operator.util.CRUDKubernetesDependentPVCResource;
+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/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/PluginCachePVCDiscriminator.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/SharedPVCDiscriminator.java
similarity index 93%
rename from operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/PluginCachePVCDiscriminator.java
rename to operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/SharedPVCDiscriminator.java
index f713e72..652f890 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/PluginCachePVCDiscriminator.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/SharedPVCDiscriminator.java
@@ -24,7 +24,7 @@
 import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
 import java.util.Optional;
 
-public class PluginCachePVCDiscriminator
+public class SharedPVCDiscriminator
     implements ResourceDiscriminator<PersistentVolumeClaim, GerritCluster> {
   @Override
   public Optional<PersistentVolumeClaim> distinguish(
@@ -37,7 +37,6 @@
                 .eventSourceRetriever()
                 .getResourceEventSourceFor(PersistentVolumeClaim.class, PVC_EVENT_SOURCE);
 
-    return ies.get(
-        new ResourceID(PluginCachePVC.PLUGIN_CACHE_PVC_NAME, primary.getMetadata().getNamespace()));
+    return ies.get(new ResourceID(SharedPVC.SHARED_PVC_NAME, primary.getMetadata().getNamespace()));
   }
 }
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/model/GerritCluster.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/model/GerritCluster.java
index 930d255..0b0f4c7 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/model/GerritCluster.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/model/GerritCluster.java
@@ -14,13 +14,14 @@
 
 package com.google.gerrit.k8s.operator.cluster.model;
 
-import static com.google.gerrit.k8s.operator.cluster.dependent.GerritLogsPVC.LOGS_PVC_NAME;
-import static com.google.gerrit.k8s.operator.cluster.dependent.GitRepositoriesPVC.REPOSITORY_PVC_NAME;
 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.cluster.GerritClusterMemberSpec;
 import com.google.gerrit.k8s.operator.shared.model.ContainerImageConfig;
+import com.google.gerrit.k8s.operator.shared.model.SharedStorage.ExternalPVCConfig;
 import io.fabric8.kubernetes.api.model.Container;
 import io.fabric8.kubernetes.api.model.ContainerBuilder;
 import io.fabric8.kubernetes.api.model.EnvVar;
@@ -42,16 +43,18 @@
 import org.apache.commons.lang3.builder.ToStringStyle;
 
 @Group("gerritoperator.google.com")
-@Version("v1alpha3")
+@Version("v1alpha14")
 @ShortNames("gclus")
 public class GerritCluster extends CustomResource<GerritClusterSpec, GerritClusterStatus>
     implements Namespaced {
   private static final long serialVersionUID = 2L;
-  private static final String GIT_REPOSITORIES_VOLUME_NAME = "git-repositories";
-  private static final String LOGS_VOLUME_NAME = "logs";
+  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);
@@ -59,13 +62,7 @@
 
   @JsonIgnore
   public Map<String, String> getLabels(String component, String createdBy) {
-    Map<String, String> labels = new HashMap<>();
-
-    labels.putAll(getSelectorLabels(component));
-    labels.put("app.kubernetes.io/version", getClass().getPackage().getImplementationVersion());
-    labels.put("app.kubernetes.io/created-by", createdBy);
-
-    return labels;
+    return getLabels(getMetadata().getName(), component, createdBy);
   }
 
   // TODO(Thomas): Having so many string parameters is bad. The only parameter should be the
@@ -76,19 +73,18 @@
     Map<String, String> labels = new HashMap<>();
 
     labels.putAll(getSelectorLabels(instance, component));
-    labels.put(
-        "app.kubernetes.io/version", GerritCluster.class.getPackage().getImplementationVersion());
+    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 Map<String, String> getSelectorLabels(String component) {
-    return getSelectorLabels(getMetadata().getName(), component);
-  }
-
-  @JsonIgnore
   public static Map<String, String> getSelectorLabels(String instance, String component) {
     Map<String, String> labels = new HashMap<>();
 
@@ -102,11 +98,12 @@
   }
 
   @JsonIgnore
-  public static Volume getGitRepositoriesVolume() {
+  public static Volume getSharedVolume(ExternalPVCConfig externalPVC) {
+    String claimName = externalPVC.isEnabled() ? externalPVC.getClaimName() : SHARED_PVC_NAME;
     return new VolumeBuilder()
-        .withName(GIT_REPOSITORIES_VOLUME_NAME)
+        .withName(SHARED_VOLUME_NAME)
         .withNewPersistentVolumeClaim()
-        .withClaimName(REPOSITORY_PVC_NAME)
+        .withClaimName(claimName)
         .endPersistentVolumeClaim()
         .build();
   }
@@ -119,18 +116,28 @@
   @JsonIgnore
   public static VolumeMount getGitRepositoriesVolumeMount(String mountPath) {
     return new VolumeMountBuilder()
-        .withName(GIT_REPOSITORIES_VOLUME_NAME)
+        .withName(SHARED_VOLUME_NAME)
+        .withSubPath("git")
         .withMountPath(mountPath)
         .build();
   }
 
   @JsonIgnore
-  public static Volume getLogsVolume() {
-    return new VolumeBuilder()
-        .withName(LOGS_VOLUME_NAME)
-        .withNewPersistentVolumeClaim()
-        .withClaimName(LOGS_PVC_NAME)
-        .endPersistentVolumeClaim()
+  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();
   }
 
@@ -142,8 +149,8 @@
   @JsonIgnore
   public static VolumeMount getLogsVolumeMount(String mountPath) {
     return new VolumeMountBuilder()
-        .withName(LOGS_VOLUME_NAME)
-        .withSubPathExpr("$(POD_NAME)")
+        .withName(SHARED_VOLUME_NAME)
+        .withSubPathExpr("logs/$(POD_NAME)")
         .withMountPath(mountPath)
         .build();
   }
@@ -183,22 +190,41 @@
   @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(
-            String.format(
-                "chown -R %d:%d /var/mnt/logs /var/mnt/git", GERRIT_FS_UID, GERRIT_FS_GID))
+        .withArgs(args.toString().trim())
         .withEnv(getPodNameEnvVar())
         .withVolumeMounts(volumeMounts)
         .build();
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/model/GerritClusterSpec.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/model/GerritClusterSpec.java
index c93e1f9..e554ad8 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/model/GerritClusterSpec.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/model/GerritClusterSpec.java
@@ -17,7 +17,9 @@
 import com.google.gerrit.k8s.operator.gerrit.model.GerritTemplate;
 import com.google.gerrit.k8s.operator.receiver.model.ReceiverTemplate;
 import com.google.gerrit.k8s.operator.shared.model.ContainerImageConfig;
+import com.google.gerrit.k8s.operator.shared.model.GerritClusterIngressConfig;
 import com.google.gerrit.k8s.operator.shared.model.GerritStorageConfig;
+import com.google.gerrit.k8s.operator.shared.model.GlobalRefDbConfig;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -26,6 +28,8 @@
   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;
 
@@ -53,6 +57,22 @@
     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;
   }
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/ConfigBuilder.java b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/ConfigBuilder.java
new file mode 100644
index 0000000..5b7f4d3
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/ConfigBuilder.java
@@ -0,0 +1,109 @@
+// 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.annotations.VisibleForTesting;
+import com.google.gerrit.k8s.operator.gerrit.model.Gerrit;
+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;
+
+@SuppressWarnings("rawtypes")
+public abstract class ConfigBuilder {
+  private final String configFileName;
+
+  private List<RequiredOption> requiredOptions = new ArrayList<>();
+  private Config config = new Config();
+
+  public ConfigBuilder(String configFileName) {
+    this.configFileName = configFileName;
+  }
+
+  abstract void addRequiredOptions(Gerrit gerrit);
+
+  public ConfigBuilder forGerrit(Gerrit gerrit) {
+    String configText = gerrit.getSpec().getConfigFiles().getOrDefault(configFileName, "");
+    this.config = parseConfig(configText);
+
+    addRequiredOptions(gerrit);
+
+    return this;
+  }
+
+  @VisibleForTesting
+  ConfigBuilder withConfig(String configText) {
+    this.config = parseConfig(configText);
+    return this;
+  }
+
+  void addRequiredOption(RequiredOption opt) {
+    requiredOptions.add(opt);
+  }
+
+  private Config parseConfig(String text) {
+    Config cfg = new Config();
+    try {
+      cfg.fromText(text);
+    } catch (ConfigInvalidException e) {
+      throw new IllegalStateException("Invalid configuration: " + text, e);
+    }
+    return cfg;
+  }
+
+  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);
+  }
+
+  @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);
+      }
+    }
+  }
+
+  public List<RequiredOption> getRequiredOptions() {
+    return requiredOptions;
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/GerritConfigValidator.java b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/ConfigValidator.java
similarity index 94%
rename from operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/GerritConfigValidator.java
rename to operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/ConfigValidator.java
index 40338b0..36e64fe 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/GerritConfigValidator.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/ConfigValidator.java
@@ -18,12 +18,12 @@
 import java.util.Set;
 import org.eclipse.jgit.lib.Config;
 
-public class GerritConfigValidator {
+public class ConfigValidator {
   @SuppressWarnings("rawtypes")
   private final List<RequiredOption> requiredOptions;
 
   @SuppressWarnings("rawtypes")
-  public GerritConfigValidator(List<RequiredOption> requiredOptions) {
+  public ConfigValidator(List<RequiredOption> requiredOptions) {
     this.requiredOptions = requiredOptions;
   }
 
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/GerritConfigBuilder.java b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/GerritConfigBuilder.java
index d6f5ca9..6386923 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/GerritConfigBuilder.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/GerritConfigBuilder.java
@@ -17,83 +17,66 @@
 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.cluster.model.GerritClusterIngressConfig.IngressType;
+import com.google.common.annotations.VisibleForTesting;
 import com.google.gerrit.k8s.operator.gerrit.dependent.GerritService;
 import com.google.gerrit.k8s.operator.gerrit.model.Gerrit;
 import com.google.gerrit.k8s.operator.gerrit.model.GerritTemplateSpec.GerritMode;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
+import com.google.gerrit.k8s.operator.shared.model.IngressConfig;
+import java.util.HashSet;
 import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Config;
 
-@SuppressWarnings("rawtypes")
-public class GerritConfigBuilder {
+public class GerritConfigBuilder extends ConfigBuilder {
   private static final Pattern PROTOCOL_PATTERN = Pattern.compile("^(https?)://.+");
-  private List<RequiredOption> requiredOptions = new ArrayList<>(setupStaticRequiredOptions());
-  private Config cfg;
 
-  private static List<RequiredOption> setupStaticRequiredOptions() {
-    List<RequiredOption> requiredOptions = new ArrayList<>();
-    requiredOptions.add(
-        new RequiredOption<String>("container", "javaHome", "/usr/lib/jvm/java-11-openjdk"));
-    requiredOptions.add(
-        new RequiredOption<Set<String>>(
-            "container",
-            "javaOptions",
-            Set.of("-Djavax.net.ssl.trustStore=/var/gerrit/etc/keystore")));
-    requiredOptions.add(new RequiredOption<String>("container", "user", "gerrit"));
-    requiredOptions.add(new RequiredOption<String>("gerrit", "basepath", "git"));
-    requiredOptions.add(new RequiredOption<String>("cache", "directory", "cache"));
-    return requiredOptions;
+  public GerritConfigBuilder() {
+    super("gerrit.config");
   }
 
-  public GerritConfigBuilder forGerrit(Gerrit gerrit) {
-    String gerritConfig = gerrit.getSpec().getConfigFiles().getOrDefault("gerrit.config", "");
+  @Override
+  void addRequiredOptions(Gerrit gerrit) {
+    String serverId = gerrit.getSpec().getServerId();
+    if (serverId != null && !serverId.isBlank()) {
+      addRequiredOption(new RequiredOption<String>("gerrit", "serverId", serverId));
+    }
 
-    withConfig(gerritConfig);
+    addRequiredOption(
+        new RequiredOption<String>("container", "javaHome", "/usr/lib/jvm/java-11-openjdk"));
+
+    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);
+    }
+    addRequiredOption(new RequiredOption<Set<String>>("container", "javaOptions", javaOptions));
+
+    addRequiredOption(new RequiredOption<String>("container", "user", "gerrit"));
+    addRequiredOption(new RequiredOption<String>("gerrit", "basepath", "git"));
+    addRequiredOption(new RequiredOption<String>("cache", "directory", "cache"));
     useReplicaMode(gerrit.getSpec().getMode().equals(GerritMode.REPLICA));
 
-    boolean ingressEnabled = gerrit.getSpec().getIngress().getType() != IngressType.NONE;
-
-    if (ingressEnabled) {
-      withUrl(gerrit.getSpec().getIngress().getUrl());
-    } else {
-      withUrl(GerritService.getUrl(gerrit));
+    withSshListenAddress(gerrit);
+    IngressConfig ingressConfig = gerrit.getSpec().getIngress();
+    if (ingressConfig.isEnabled()) {
+      withUrl(ingressConfig.getUrl());
+      withSshAdvertisedAddress(gerrit);
     }
-
-    if (ingressEnabled && gerrit.getSpec().getIngress().getType() == IngressType.ISTIO) {
-      withSsh(
-          gerrit.getSpec().getService().isSshEnabled(),
-          gerrit.getSpec().getIngress().getFullHostnameForService(GerritService.getName(gerrit))
-              + ":29418");
-    } else {
-      withSsh(gerrit.getSpec().getService().isSshEnabled());
-    }
-    return this;
   }
 
-  public GerritConfigBuilder withConfig(String text) {
-    Config cfg = new Config();
-    try {
-      cfg.fromText(text);
-    } catch (ConfigInvalidException e) {
-      throw new IllegalStateException("The provided gerrit.config is invalid.");
-    }
-
-    return withConfig(cfg);
-  }
-
-  public GerritConfigBuilder withConfig(Config cfg) {
-    this.cfg = cfg;
-    return this;
-  }
-
-  public GerritConfigBuilder withUrl(String url) {
-    requiredOptions.add(new RequiredOption<String>("gerrit", "canonicalWebUrl", url));
+  @VisibleForTesting
+  GerritConfigBuilder withUrl(String url) {
+    addRequiredOption(new RequiredOption<String>("gerrit", "canonicalWebUrl", url));
 
     StringBuilder listenUrlBuilder = new StringBuilder();
     listenUrlBuilder.append("proxy-");
@@ -107,71 +90,34 @@
     listenUrlBuilder.append("://*:");
     listenUrlBuilder.append(HTTP_PORT);
     listenUrlBuilder.append("/");
-    requiredOptions.add(
+    addRequiredOption(
         new RequiredOption<String>("httpd", "listenUrl", listenUrlBuilder.toString()));
     return this;
   }
 
-  public GerritConfigBuilder withSsh(boolean enabled) {
+  private void withSshListenAddress(Gerrit gerrit) {
     String listenAddress;
-    if (enabled) {
+    if (gerrit.isSshEnabled()) {
       listenAddress = "*:" + SSH_PORT;
     } else {
       listenAddress = "off";
     }
-    requiredOptions.add(new RequiredOption<String>("sshd", "listenAddress", listenAddress));
-    return this;
+    addRequiredOption(new RequiredOption<String>("sshd", "listenAddress", listenAddress));
   }
 
-  public GerritConfigBuilder withSsh(boolean enabled, String advertisedAddress) {
-    requiredOptions.add(new RequiredOption<String>("sshd", "advertisedAddress", advertisedAddress));
-    return withSsh(enabled);
-  }
-
-  public GerritConfigBuilder useReplicaMode(boolean isReplica) {
-    requiredOptions.add(new RequiredOption<Boolean>("container", "replica", isReplica));
-    return this;
-  }
-
-  public Config build() {
-    GerritConfigValidator configValidator = new GerritConfigValidator(requiredOptions);
-    try {
-      configValidator.check(cfg);
-    } catch (InvalidGerritConfigException e) {
-      throw new IllegalStateException(e);
-    }
-    setRequiredOptions();
-    return cfg;
-  }
-
-  public void validate() throws InvalidGerritConfigException {
-    new GerritConfigValidator(requiredOptions).check(cfg);
-  }
-
-  @SuppressWarnings("unchecked")
-  private void setRequiredOptions() {
-    for (RequiredOption<?> opt : requiredOptions) {
-      if (opt.getExpected() instanceof String) {
-        cfg.setString(
-            opt.getSection(), opt.getSubSection(), opt.getKey(), (String) opt.getExpected());
-      } else if (opt.getExpected() instanceof Boolean) {
-        cfg.setBoolean(
-            opt.getSection(), opt.getSubSection(), opt.getKey(), (Boolean) opt.getExpected());
-      } else if (opt.getExpected() instanceof Set) {
-        List<String> values =
-            new ArrayList<String>(
-                Arrays.asList(
-                    cfg.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);
-        cfg.setStringList(opt.getSection(), opt.getSubSection(), opt.getKey(), values);
-      }
+  private void withSshAdvertisedAddress(Gerrit gerrit) {
+    if (gerrit.isSshEnabled()) {
+      addRequiredOption(
+          new RequiredOption<String>(
+              "sshd",
+              "advertisedAddress",
+              gerrit.getSpec().getIngress().getFullHostnameForService(GerritService.getName(gerrit))
+                  + ":29418"));
     }
   }
 
-  public List<RequiredOption> getRequiredOptions() {
-    return requiredOptions;
+  private GerritConfigBuilder useReplicaMode(boolean isReplica) {
+    addRequiredOption(new RequiredOption<Boolean>("container", "replica", isReplica));
+    return this;
   }
 }
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/HighAvailabilityPluginConfigBuilder.java b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/HighAvailabilityPluginConfigBuilder.java
new file mode 100644
index 0000000..d5a02d6
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/HighAvailabilityPluginConfigBuilder.java
@@ -0,0 +1,74 @@
+// 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.gerrit.k8s.operator.gerrit.dependent.GerritStatefulSet;
+import com.google.gerrit.k8s.operator.gerrit.model.Gerrit;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+public class HighAvailabilityPluginConfigBuilder extends PluginConfigBuilder {
+  public HighAvailabilityPluginConfigBuilder() {
+    super("high-availability");
+  }
+
+  @Override
+  void addRequiredOptions(Gerrit gerrit) {
+    addRequiredOption(
+        new RequiredPluginOption<String>("high-availability", "main", "sharedDirectory", "shared"));
+    addRequiredOption(
+        new RequiredPluginOption<String>("high-availability", "peerInfo", "strategy", "jgroups"));
+    addRequiredOption(
+        new RequiredPluginOption<String>(
+            "high-availability", "peerInfo", "jgroups", "myUrl", null));
+    addRequiredOption(
+        new RequiredPluginOption<String>(
+            "high-availability", "jgroups", "clusterName", gerrit.getMetadata().getName()));
+    addRequiredOption(
+        new RequiredPluginOption<Boolean>("high-availability", "jgroups", "kubernetes", true));
+    addRequiredOption(
+        new RequiredPluginOption<String>(
+            "high-availability",
+            "jgroups",
+            "kubernetes",
+            "namespace",
+            gerrit.getMetadata().getNamespace()));
+    addRequiredOption(
+        new RequiredPluginOption<Set<String>>(
+            "high-availability", "jgroups", "kubernetes", "label", getLabels(gerrit)));
+    addRequiredOption(
+        new RequiredPluginOption<Boolean>("high-availability", "cache", "synchronize", true));
+    addRequiredOption(
+        new RequiredPluginOption<Boolean>("high-availability", "event", "synchronize", true));
+    addRequiredOption(
+        new RequiredPluginOption<Boolean>("high-availability", "index", "synchronize", true));
+    addRequiredOption(
+        new RequiredPluginOption<Boolean>("high-availability", "index", "synchronizeForced", true));
+    addRequiredOption(
+        new RequiredPluginOption<Boolean>("high-availability", "healthcheck", "enable", true));
+    addRequiredOption(
+        new RequiredPluginOption<Boolean>("high-availability", "ref-database", "enabled", true));
+  }
+
+  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/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/OptionalSharedStorage.java b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/PluginConfigBuilder.java
similarity index 62%
copy from operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/OptionalSharedStorage.java
copy to operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/PluginConfigBuilder.java
index b4c7365..4ad5e17 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/OptionalSharedStorage.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/PluginConfigBuilder.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2022 The Android Open Source Project
+// 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.
@@ -12,17 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.k8s.operator.shared.model;
+package com.google.gerrit.k8s.operator.gerrit.config;
 
-public class OptionalSharedStorage extends SharedStorage {
+public abstract class PluginConfigBuilder extends ConfigBuilder {
 
-  private boolean enabled = false;
-
-  public boolean isEnabled() {
-    return enabled;
-  }
-
-  public void setEnabled(boolean enabled) {
-    this.enabled = enabled;
+  public PluginConfigBuilder(String pluginName) {
+    super(pluginName + ".config");
   }
 }
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/RequiredPluginOption.java b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/RequiredPluginOption.java
new file mode 100644
index 0000000..2e98e27
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/RequiredPluginOption.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.gerrit.config;
+
+public class RequiredPluginOption<T> extends RequiredOption<T> {
+  private final String plugin;
+
+  public RequiredPluginOption(
+      String plugin, String section, String subSection, String key, T expected) {
+    super(section, subSection, key, expected);
+    this.plugin = plugin;
+  }
+
+  public RequiredPluginOption(String plugin, String section, String key, T expected) {
+    super(section, null, key, expected);
+    this.plugin = plugin;
+  }
+
+  public String getPlugin() {
+    return plugin;
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/ZookeeperRefDbPluginConfigBuilder.java b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/ZookeeperRefDbPluginConfigBuilder.java
new file mode 100644
index 0000000..0de42eb
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/ZookeeperRefDbPluginConfigBuilder.java
@@ -0,0 +1,41 @@
+// 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.gerrit.k8s.operator.gerrit.model.Gerrit;
+
+public class ZookeeperRefDbPluginConfigBuilder extends PluginConfigBuilder {
+  public ZookeeperRefDbPluginConfigBuilder() {
+    super("zookeeper-refdb");
+  }
+
+  @Override
+  void addRequiredOptions(Gerrit gerrit) {
+    addRequiredOption(
+        new RequiredPluginOption<String>(
+            "zookeeper-refdb",
+            "ref-database",
+            "zookeeper",
+            "connectString",
+            gerrit.getSpec().getRefdb().getZookeeper().getConnectString()));
+    addRequiredOption(
+        new RequiredPluginOption<String>(
+            "zookeeper-refdb",
+            "ref-database",
+            "zookeeper",
+            "rootNode",
+            gerrit.getSpec().getRefdb().getZookeeper().getRootNode()));
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritConfigMap.java b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritConfigMap.java
index 330f972..ea01a70 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritConfigMap.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritConfigMap.java
@@ -15,8 +15,12 @@
 package com.google.gerrit.k8s.operator.gerrit.dependent;
 
 import com.google.gerrit.k8s.operator.cluster.model.GerritCluster;
+import com.google.gerrit.k8s.operator.gerrit.config.ConfigBuilder;
 import com.google.gerrit.k8s.operator.gerrit.config.GerritConfigBuilder;
+import com.google.gerrit.k8s.operator.gerrit.config.HighAvailabilityPluginConfigBuilder;
+import com.google.gerrit.k8s.operator.gerrit.config.ZookeeperRefDbPluginConfigBuilder;
 import com.google.gerrit.k8s.operator.gerrit.model.Gerrit;
+import com.google.gerrit.k8s.operator.shared.model.GlobalRefDbConfig;
 import io.fabric8.kubernetes.api.model.ConfigMap;
 import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
 import io.javaoperatorsdk.operator.api.reconciler.Context;
@@ -45,10 +49,22 @@
       configFiles.put("gerrit.config", "");
     }
 
-    GerritConfigBuilder gerritConfigBuilder = new GerritConfigBuilder().forGerrit(gerrit);
+    ConfigBuilder gerritConfigBuilder = new GerritConfigBuilder().forGerrit(gerrit);
 
     configFiles.put("gerrit.config", gerritConfigBuilder.build().toText());
 
+    if (gerrit.getSpec().isHighlyAvailablePrimary()) {
+      configFiles.put(
+          "high-availability.config",
+          new HighAvailabilityPluginConfigBuilder().forGerrit(gerrit).build().toText());
+    }
+
+    if (gerrit.getSpec().getRefdb().getDatabase().equals(GlobalRefDbConfig.RefDatabase.ZOOKEEPER)) {
+      configFiles.put(
+          "zookeeper-refdb.config",
+          new ZookeeperRefDbPluginConfigBuilder().forGerrit(gerrit).build().toText());
+    }
+
     if (!configFiles.containsKey("healthcheck.config")) {
       configFiles.put("healthcheck.config", DEFAULT_HEALTHCHECK_CONFIG);
     }
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritInitConfigMap.java b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritInitConfigMap.java
index 58ea057..65c5249 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritInitConfigMap.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritInitConfigMap.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.k8s.operator.gerrit.dependent;
 
+import static com.google.gerrit.k8s.operator.cluster.model.GerritCluster.PLUGIN_CACHE_MOUNT_PATH;
+
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
@@ -22,13 +24,14 @@
 import com.google.gerrit.k8s.operator.cluster.model.GerritCluster;
 import com.google.gerrit.k8s.operator.gerrit.model.Gerrit;
 import com.google.gerrit.k8s.operator.gerrit.model.GerritInitConfig;
+import com.google.gerrit.k8s.operator.shared.model.GlobalRefDbConfig;
 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;
-import java.util.stream.Collectors;
 
 @KubernetesDependent(resourceDiscriminator = GerritInitConfigMapDiscriminator.class)
 public class GerritInitConfigMap extends CRUDKubernetesDependentResource<ConfigMap, Gerrit> {
@@ -57,21 +60,15 @@
 
   private String getGerritInitConfig(Gerrit gerrit) {
     GerritInitConfig config = new GerritInitConfig();
-    config.setDownloadedPlugins(
-        gerrit.getSpec().getPlugins().stream()
-            .filter(p -> !p.isPackagedPlugin())
-            .collect(Collectors.toSet()));
-    config.setPackagedPlugins(
-        gerrit.getSpec().getPlugins().stream()
-            .filter(p -> p.isPackagedPlugin())
-            .map(p -> p.getName())
-            .collect(Collectors.toSet()));
-    config.setPluginCacheEnabled(gerrit.getSpec().getStorage().getPluginCacheStorage().isEnabled());
-    config.setInstallAsLibrary(
-        gerrit.getSpec().getPlugins().stream()
-            .filter(p -> p.isInstallAsLibrary())
-            .map(p -> p.getName())
-            .collect(Collectors.toSet()));
+    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());
+
+    if (gerrit.getSpec().getRefdb().getDatabase().equals(GlobalRefDbConfig.RefDatabase.ZOOKEEPER)) {
+      config.setRefdb(GlobalRefDbConfig.RefDatabase.ZOOKEEPER.toString().toLowerCase(Locale.US));
+    }
 
     ObjectMapper mapper =
         new ObjectMapper(new YAMLFactory().disable(Feature.WRITE_DOC_START_MARKER));
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritService.java b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritService.java
index 65ea5e4..92883f6 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritService.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritService.java
@@ -61,6 +61,10 @@
     return gerrit.getMetadata().getName();
   }
 
+  public static String getName(String gerritName) {
+    return gerritName;
+  }
+
   public static String getName(GerritTemplate gerrit) {
     return gerrit.getMetadata().getName();
   }
@@ -69,7 +73,7 @@
     return getHostname(gerrit.getMetadata().getName(), gerrit.getMetadata().getNamespace());
   }
 
-  private static String getHostname(String name, String namespace) {
+  public static String getHostname(String name, String namespace) {
     return String.format("%s.%s.svc.cluster.local", name, namespace);
   }
 
@@ -91,7 +95,7 @@
             .withPort(gerrit.getSpec().getService().getHttpPort())
             .withNewTargetPort(HTTP_PORT)
             .build());
-    if (gerrit.getSpec().getService().isSshEnabled()) {
+    if (gerrit.isSshEnabled()) {
       ports.add(
           new ServicePortBuilder()
               .withName("ssh")
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritStatefulSet.java b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritStatefulSet.java
index 6cb9e72..d12b8f8 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritStatefulSet.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritStatefulSet.java
@@ -17,13 +17,15 @@
 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.cluster.dependent.PluginCachePVC;
 import com.google.gerrit.k8s.operator.cluster.model.GerritCluster;
 import com.google.gerrit.k8s.operator.gerrit.GerritReconciler;
 import com.google.gerrit.k8s.operator.gerrit.model.Gerrit;
+import com.google.gerrit.k8s.operator.shared.model.ContainerImageConfig;
 import com.google.gerrit.k8s.operator.shared.model.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;
@@ -52,6 +54,8 @@
   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);
@@ -66,11 +70,19 @@
     NfsWorkaroundConfig nfsWorkaround =
         gerrit.getSpec().getStorage().getStorageClasses().getNfsWorkaround();
     if (nfsWorkaround.isEnabled() && nfsWorkaround.isChownOnStartup()) {
-      initContainers.add(
-          GerritCluster.createNfsInitContainer(
-              gerrit.getSpec().getStorage().getStorageClasses().getNfsWorkaround().getIdmapdConfig()
-                  != null,
-              gerrit.getSpec().getContainerImages()));
+      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<>();
@@ -114,6 +126,7 @@
         .withLabels(getLabels(gerrit))
         .endMetadata()
         .withNewSpec()
+        .withServiceAccount(gerrit.getSpec().getServiceAccount())
         .withTolerations(gerrit.getSpec().getTolerations())
         .withTopologySpreadConstraints(gerrit.getSpec().getTopologySpreadConstraints())
         .withAffinity(gerrit.getSpec().getAffinity())
@@ -125,7 +138,7 @@
         .endSecurityContext()
         .addNewInitContainer()
         .withName("gerrit-init")
-        .withEnv(GerritCluster.getPodNameEnvVar())
+        .withEnv(getEnvVars(gerrit))
         .withImagePullPolicy(gerrit.getSpec().getContainerImages().getImagePullPolicy())
         .withImage(
             gerrit.getSpec().getContainerImages().getGerritImages().getFullImageName("gerrit-init"))
@@ -146,7 +159,7 @@
         .endExec()
         .endPreStop()
         .endLifecycle()
-        .withEnv(GerritCluster.getPodNameEnvVar())
+        .withEnv(getEnvVars(gerrit))
         .withPorts(getContainerPorts(gerrit))
         .withResources(gerrit.getSpec().getResources())
         .withStartupProbe(gerrit.getSpec().getStartupProbe())
@@ -194,8 +207,9 @@
   private Set<Volume> getVolumes(Gerrit gerrit) {
     Set<Volume> volumes = new HashSet<>();
 
-    volumes.add(GerritCluster.getGitRepositoriesVolume());
-    volumes.add(GerritCluster.getLogsVolume());
+    volumes.add(
+        GerritCluster.getSharedVolume(
+            gerrit.getSpec().getStorage().getSharedStorage().getExternalPVC()));
 
     volumes.add(
         new VolumeBuilder()
@@ -221,17 +235,6 @@
             .endSecret()
             .build());
 
-    if (gerrit.getSpec().getStorage().getPluginCacheStorage().isEnabled()
-        && gerrit.getSpec().getPlugins().stream().anyMatch(p -> !p.isPackagedPlugin())) {
-      volumes.add(
-          new VolumeBuilder()
-              .withName("gerrit-plugin-cache")
-              .withNewPersistentVolumeClaim()
-              .withClaimName(PluginCachePVC.PLUGIN_CACHE_PVC_NAME)
-              .endPersistentVolumeClaim()
-              .build());
-    }
-
     NfsWorkaroundConfig nfsWorkaround =
         gerrit.getSpec().getStorage().getStorageClasses().getNfsWorkaround();
     if (nfsWorkaround.isEnabled() && nfsWorkaround.getIdmapdConfig() != null) {
@@ -245,6 +248,9 @@
     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(
@@ -266,13 +272,9 @@
               .withMountPath("/var/config")
               .build());
 
-      if (gerrit.getSpec().getStorage().getPluginCacheStorage().isEnabled()
+      if (gerrit.getSpec().getStorage().getPluginCache().isEnabled()
           && gerrit.getSpec().getPlugins().stream().anyMatch(p -> !p.isPackagedPlugin())) {
-        volumeMounts.add(
-            new VolumeMountBuilder()
-                .withName("gerrit-plugin-cache")
-                .withMountPath("/var/mnt/plugins")
-                .build());
+        volumeMounts.add(GerritCluster.getPluginCacheVolumeMount());
       }
     }
 
@@ -289,13 +291,36 @@
     List<ContainerPort> containerPorts = new ArrayList<>();
     containerPorts.add(new ContainerPort(HTTP_PORT, null, null, "http", null));
 
-    if (gerrit.getSpec().getService().isSshEnabled()) {
+    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)) {
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/Gerrit.java b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/Gerrit.java
index 943eb56..9d1bf80 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/Gerrit.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/Gerrit.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.k8s.operator.gerrit.model;
 
+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;
@@ -23,7 +24,7 @@
 import org.apache.commons.lang3.builder.ToStringStyle;
 
 @Group("gerritoperator.google.com")
-@Version("v1alpha4")
+@Version("v1alpha16")
 @ShortNames("gcr")
 public class Gerrit extends CustomResource<GerritSpec, GerritStatus> implements Namespaced {
   private static final long serialVersionUID = 2L;
@@ -31,4 +32,9 @@
   public String toString() {
     return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE);
   }
+
+  @JsonIgnore
+  public boolean isSshEnabled() {
+    return getSpec().getService().getSshPort() > 0;
+  }
 }
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/model/GerritIngressTlsConfig.java b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/GerritDebugConfig.java
similarity index 68%
copy from operator/src/main/java/com/google/gerrit/k8s/operator/cluster/model/GerritIngressTlsConfig.java
copy to operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/GerritDebugConfig.java
index 6284b0a..69d32ad 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/model/GerritIngressTlsConfig.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/GerritDebugConfig.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2022 The Android Open Source Project
+// 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.
@@ -12,12 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.k8s.operator.cluster.model;
+package com.google.gerrit.k8s.operator.gerrit.model;
 
-public class GerritIngressTlsConfig {
-
-  private boolean enabled = false;
-  private String secret;
+public class GerritDebugConfig {
+  private boolean enabled;
+  private boolean suspend;
 
   public boolean isEnabled() {
     return enabled;
@@ -27,11 +26,11 @@
     this.enabled = enabled;
   }
 
-  public String getSecret() {
-    return secret;
+  public boolean isSuspend() {
+    return suspend;
   }
 
-  public void setSecret(String secret) {
-    this.secret = secret;
+  public void setSuspend(boolean suspend) {
+    this.suspend = suspend;
   }
 }
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/GerritInitConfig.java b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/GerritInitConfig.java
index 52f10ad..07d5270 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/GerritInitConfig.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/GerritInitConfig.java
@@ -14,15 +14,20 @@
 
 package com.google.gerrit.k8s.operator.gerrit.model;
 
-import java.util.Set;
+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 Set<String> packagedPlugins;
-  private Set<GerritPlugin> downloadedPlugins;
-  private Set<String> installAsLibrary;
+  private List<GerritPlugin> plugins;
+  private List<GerritModule> libs;
+
+  @JsonProperty("highAvailability")
+  private boolean isHighlyAvailable;
+
+  private String refdb;
 
   public String getCaCertPath() {
     return caCertPath;
@@ -48,27 +53,37 @@
     this.pluginCacheDir = pluginCacheDir;
   }
 
-  public Set<String> getPackagedPlugins() {
-    return packagedPlugins;
+  public List<GerritPlugin> getPlugins() {
+    return plugins;
   }
 
-  public void setPackagedPlugins(Set<String> packagedPlugins) {
-    this.packagedPlugins = packagedPlugins;
+  public void setPlugins(List<GerritPlugin> plugins) {
+    this.plugins = plugins;
   }
 
-  public Set<GerritPlugin> getDownloadedPlugins() {
-    return downloadedPlugins;
+  public List<GerritModule> getLibs() {
+    return libs;
   }
 
-  public void setDownloadedPlugins(Set<GerritPlugin> downloadedPlugins) {
-    this.downloadedPlugins = downloadedPlugins;
+  public void setLibs(List<GerritModule> libs) {
+    this.libs = libs;
   }
 
-  public Set<String> getInstallAsLibrary() {
-    return installAsLibrary;
+  @JsonProperty("highAvailability")
+  public boolean isHighlyAvailable() {
+    return isHighlyAvailable;
   }
 
-  public void setInstallAsLibrary(Set<String> installAsLibrary) {
-    this.installAsLibrary = installAsLibrary;
+  @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/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/GerritModule.java b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/GerritModule.java
new file mode 100644
index 0000000..68e4c0f
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/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.gerrit.model;
+
+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/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/GerritPlugin.java b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/GerritPlugin.java
index 0d5547e..9fd2087 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/GerritPlugin.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/GerritPlugin.java
@@ -15,51 +15,22 @@
 package com.google.gerrit.k8s.operator.gerrit.model;
 
 import com.fasterxml.jackson.annotation.JsonIgnore;
-import java.io.Serializable;
-import java.net.URL;
+import com.fasterxml.jackson.annotation.JsonInclude;
 
-public class GerritPlugin implements Serializable {
+public class GerritPlugin extends GerritModule {
   private static final long serialVersionUID = 1L;
 
-  private String name;
-  private URL url;
-  private String sha1;
+  @JsonInclude(JsonInclude.Include.NON_EMPTY)
   private boolean installAsLibrary = false;
 
   public GerritPlugin() {}
 
   public GerritPlugin(String name) {
-    this.name = name;
+    super(name);
   }
 
-  public GerritPlugin(String name, URL 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 URL getUrl() {
-    return url;
-  }
-
-  public void setUrl(URL url) {
-    this.url = url;
-  }
-
-  public String getSha1() {
-    return sha1;
-  }
-
-  public void setSha1(String sha1) {
-    this.sha1 = sha1;
+  public GerritPlugin(String name, String url, String sha1) {
+    super(name, url, sha1);
   }
 
   public boolean isInstallAsLibrary() {
@@ -72,6 +43,6 @@
 
   @JsonIgnore
   public boolean isPackagedPlugin() {
-    return url == null;
+    return getUrl() == null;
   }
 }
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/GerritServiceConfig.java b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/GerritServiceConfig.java
deleted file mode 100644
index 3ffdf2a..0000000
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/GerritServiceConfig.java
+++ /dev/null
@@ -1,55 +0,0 @@
-// 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.model;
-
-import com.fasterxml.jackson.annotation.JsonIgnore;
-import java.io.Serializable;
-
-public class GerritServiceConfig implements Serializable {
-  private static final long serialVersionUID = 1L;
-
-  String type = "NodePort";
-  int httpPort = 80;
-  Integer sshPort;
-
-  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;
-  }
-
-  public Integer getSshPort() {
-    return sshPort;
-  }
-
-  public void setSshPort(int sshPort) {
-    this.sshPort = sshPort;
-  }
-
-  @JsonIgnore
-  public boolean isSshEnabled() {
-    return sshPort != null && sshPort > 0;
-  }
-}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/GerritSpec.java b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/GerritSpec.java
index 5068b09..58f5538 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/GerritSpec.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/GerritSpec.java
@@ -16,12 +16,15 @@
 
 import com.google.gerrit.k8s.operator.shared.model.ContainerImageConfig;
 import com.google.gerrit.k8s.operator.shared.model.GerritStorageConfig;
+import com.google.gerrit.k8s.operator.shared.model.GlobalRefDbConfig;
 import com.google.gerrit.k8s.operator.shared.model.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() {}
 
@@ -52,4 +55,20 @@
   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/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/GerritTemplate.java b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/GerritTemplate.java
index 39329e7..4c1fa12 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/GerritTemplate.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/GerritTemplate.java
@@ -20,6 +20,7 @@
 import com.fasterxml.jackson.annotation.JsonPropertyOrder;
 import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
 import com.google.gerrit.k8s.operator.cluster.model.GerritCluster;
+import com.google.gerrit.k8s.operator.shared.model.GlobalRefDbConfig;
 import com.google.gerrit.k8s.operator.shared.model.IngressConfig;
 import io.fabric8.kubernetes.api.model.KubernetesResource;
 import io.fabric8.kubernetes.api.model.ObjectMeta;
@@ -74,10 +75,24 @@
     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.setType(gerritCluster.getSpec().getIngress().getType());
     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;
   }
@@ -90,4 +105,11 @@
         .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/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/GerritTemplateSpec.java b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/GerritTemplateSpec.java
index 95c5fb8..4ae5737 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/GerritTemplateSpec.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/GerritTemplateSpec.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.k8s.operator.gerrit.model;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.google.gerrit.k8s.operator.shared.model.HttpSshServiceConfig;
 import io.fabric8.kubernetes.api.model.Affinity;
 import io.fabric8.kubernetes.api.model.ResourceRequirements;
 import io.fabric8.kubernetes.api.model.Toleration;
@@ -23,6 +25,8 @@
 import java.util.Map;
 
 public class GerritTemplateSpec {
+  private String serviceAccount;
+
   private List<Toleration> tolerations;
   private Affinity affinity;
   private List<TopologySpreadConstraint> topologySpreadConstraints = new ArrayList<>();
@@ -39,17 +43,21 @@
 
   private long gracefulStopTimeout = 30L;
 
-  private GerritServiceConfig service = new GerritServiceConfig();
+  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;
@@ -70,9 +78,20 @@
 
     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() {
@@ -164,11 +183,11 @@
     this.gracefulStopTimeout = gracefulStopTimeout;
   }
 
-  public GerritServiceConfig getService() {
+  public HttpSshServiceConfig getService() {
     return service;
   }
 
-  public void setService(GerritServiceConfig service) {
+  public void setService(HttpSshServiceConfig service) {
     this.service = service;
   }
 
@@ -188,6 +207,14 @@
     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;
   }
@@ -212,8 +239,21 @@
     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/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/dependent/GitGarbageCollectionCronJob.java b/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/dependent/GitGarbageCollectionCronJob.java
index 67d84eb..5a39d3a 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/dependent/GitGarbageCollectionCronJob.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/dependent/GitGarbageCollectionCronJob.java
@@ -51,27 +51,14 @@
         gerritCluster.getLabels("GitGc", this.getClass().getSimpleName());
 
     List<Container> initContainers = new ArrayList<>();
-    List<Volume> volumes =
-        List.of(GerritCluster.getGitRepositoriesVolume(), GerritCluster.getLogsVolume());
-
-    if (gerritCluster.getSpec().getStorage().getStorageClasses().getNfsWorkaround().isEnabled()) {
-      if (gerritCluster
-          .getSpec()
-          .getStorage()
-          .getStorageClasses()
-          .getNfsWorkaround()
-          .isChownOnStartup()) {
-        initContainers.add(gerritCluster.createNfsInitContainer());
-      }
-      if (gerritCluster
-              .getSpec()
-              .getStorage()
-              .getStorageClasses()
-              .getNfsWorkaround()
-              .getIdmapdConfig()
-          != null) {
-        volumes.add(GerritCluster.getNfsImapdConfigVolume());
-      }
+    if (gerritCluster.getSpec().getStorage().getStorageClasses().getNfsWorkaround().isEnabled()
+        && gerritCluster
+            .getSpec()
+            .getStorage()
+            .getStorageClasses()
+            .getNfsWorkaround()
+            .isChownOnStartup()) {
+      initContainers.add(gerritCluster.createNfsInitContainer());
     }
 
     JobTemplateSpec gitGcJobTemplate =
@@ -97,7 +84,7 @@
             .withFsGroup(100L)
             .endSecurityContext()
             .addToContainers(buildGitGcContainer(gitGc, gerritCluster))
-            .withVolumes(volumes)
+            .withVolumes(getVolumes(gerritCluster))
             .endSpec()
             .endTemplate()
             .endSpec()
@@ -170,4 +157,25 @@
 
     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/operator/src/main/java/com/google/gerrit/k8s/operator/network/GerritClusterIngressCondition.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/GerritClusterIngressCondition.java
new file mode 100644
index 0000000..265c8e1
--- /dev/null
+++ b/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.network.model.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/operator/src/main/java/com/google/gerrit/k8s/operator/network/GerritNetworkReconcilerProvider.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/GerritNetworkReconcilerProvider.java
new file mode 100644
index 0000000..5390bd3
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/network/GerritNetworkReconcilerProvider.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.network;
+
+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.model.GerritNetwork;
+import com.google.gerrit.k8s.operator.network.none.GerritNoIngressReconciler;
+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();
+      default:
+        return new GerritNoIngressReconciler();
+    }
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/OptionalSharedStorage.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/IngressType.java
similarity index 62%
copy from operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/OptionalSharedStorage.java
copy to operator/src/main/java/com/google/gerrit/k8s/operator/network/IngressType.java
index b4c7365..a4af09e 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/OptionalSharedStorage.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/network/IngressType.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2022 The Android Open Source Project
+// 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.
@@ -12,17 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.k8s.operator.shared.model;
+package com.google.gerrit.k8s.operator.network;
 
-public class OptionalSharedStorage extends SharedStorage {
-
-  private boolean enabled = false;
-
-  public boolean isEnabled() {
-    return enabled;
-  }
-
-  public void setEnabled(boolean enabled) {
-    this.enabled = enabled;
-  }
+public enum IngressType {
+  NONE,
+  INGRESS,
+  ISTIO
 }
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/network/ingress/GerritIngressReconciler.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ingress/GerritIngressReconciler.java
new file mode 100644
index 0000000..64d7d2b
--- /dev/null
+++ b/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.network.model.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/operator/src/main/java/com/google/gerrit/k8s/operator/network/ingress/dependent/GerritClusterIngress.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ingress/dependent/GerritClusterIngress.java
new file mode 100644
index 0000000..04102d8
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ingress/dependent/GerritClusterIngress.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.network.ingress.dependent;
+
+import static com.google.gerrit.k8s.operator.network.model.GerritNetwork.SESSION_COOKIE_NAME;
+
+import com.google.gerrit.k8s.operator.cluster.model.GerritCluster;
+import com.google.gerrit.k8s.operator.gerrit.dependent.GerritService;
+import com.google.gerrit.k8s.operator.network.model.GerritNetwork;
+import com.google.gerrit.k8s.operator.receiver.dependent.ReceiverService;
+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";
+  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");
+
+    if (gerritNetwork.hasPrimaryGerrit() && gerritNetwork.hasGerritReplica()) {
+      String svcName = GerritService.getName(gerritNetwork.getSpec().getGerritReplica().getName());
+      annotations.put(
+          "nginx.ingress.kubernetes.io/configuration-snippet",
+          createNginxConfigSnippet(gerritNetwork.getMetadata().getNamespace(), svcName));
+    }
+
+    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 namespace, String svcName) {
+    StringBuilder configSnippet = new StringBuilder();
+    configSnippet.append("if ($args ~ service=git-upload-pack){");
+    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", "/new", "/git")) {
+      paths.add(
+          new HTTPIngressPathBuilder()
+              .withPathType("Prefix")
+              .withPath(path)
+              .withNewBackend()
+              .withNewService()
+              .withName(svcName)
+              .withPort(port)
+              .endService()
+              .endBackend()
+              .build());
+    }
+    return paths;
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/GerritIstioReconciler.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/GerritIstioReconciler.java
new file mode 100644
index 0000000..adbbf72
--- /dev/null
+++ b/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.network.model.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/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritClusterIstioGateway.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritClusterIstioGateway.java
new file mode 100644
index 0000000..a300f7d
--- /dev/null
+++ b/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.cluster.model.GerritCluster;
+import com.google.gerrit.k8s.operator.network.model.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/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioCondition.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritIstioCondition.java
similarity index 63%
rename from operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioCondition.java
rename to operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritIstioCondition.java
index 4bd5154..eb632e9 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioCondition.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritIstioCondition.java
@@ -12,24 +12,23 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.k8s.operator.cluster.dependent;
+package com.google.gerrit.k8s.operator.network.istio.dependent;
 
-import com.google.gerrit.k8s.operator.cluster.model.GerritCluster;
-import com.google.gerrit.k8s.operator.cluster.model.GerritClusterIngressConfig.IngressType;
+import com.google.gerrit.k8s.operator.network.model.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, GerritCluster> {
+public class GerritIstioCondition implements Condition<VirtualService, GerritNetwork> {
 
   @Override
   public boolean isMet(
-      DependentResource<VirtualService, GerritCluster> dependentResource,
-      GerritCluster gerritCluster,
-      Context<GerritCluster> context) {
-    return gerritCluster.getSpec().getIngress().isEnabled()
-        && gerritCluster.getSpec().getIngress().getType() == IngressType.ISTIO
-        && !gerritCluster.getSpec().getGerrits().isEmpty();
+      DependentResource<VirtualService, GerritNetwork> dependentResource,
+      GerritNetwork gerritNetwork,
+      Context<GerritNetwork> context) {
+
+    return gerritNetwork.getSpec().getIngress().isEnabled()
+        && (gerritNetwork.hasGerrits() || gerritNetwork.hasReceiver());
   }
 }
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritIstioDestinationRule.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritIstioDestinationRule.java
new file mode 100644
index 0000000..087719d
--- /dev/null
+++ b/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.network.model.GerritNetwork.SESSION_COOKIE_NAME;
+import static com.google.gerrit.k8s.operator.network.model.GerritNetwork.SESSION_COOKIE_TTL;
+
+import com.google.gerrit.k8s.operator.cluster.model.GerritCluster;
+import com.google.gerrit.k8s.operator.gerrit.dependent.GerritService;
+import com.google.gerrit.k8s.operator.gerrit.model.GerritTemplate;
+import com.google.gerrit.k8s.operator.network.model.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/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritIstioVirtualService.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritIstioVirtualService.java
new file mode 100644
index 0000000..6e55f5b
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritIstioVirtualService.java
@@ -0,0 +1,223 @@
+// 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.cluster.model.GerritCluster;
+import com.google.gerrit.k8s.operator.gerrit.dependent.GerritService;
+import com.google.gerrit.k8s.operator.network.model.GerritNetwork;
+import com.google.gerrit.k8s.operator.network.model.NetworkMember;
+import com.google.gerrit.k8s.operator.network.model.NetworkMemberWithSsh;
+import com.google.gerrit.k8s.operator.receiver.dependent.ReceiverService;
+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 UPLOAD_PACK_INFO_REF_URL_PATTERN = "^/(.*)/info/refs$";
+  private static final String UPLOAD_PACK_URL_PATTERN = "^/(.*)/git-upload-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(UPLOAD_PACK_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("/git/").build())
+            .build());
+    matches.add(
+        new HTTPMatchRequestBuilder()
+            .withUri(new StringMatchBuilder().withNewStringMatchPrefixType("/a/projects/").build())
+            .build());
+    matches.add(
+        new HTTPMatchRequestBuilder()
+            .withUri(new StringMatchBuilder().withNewStringMatchPrefixType("/new/").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/operator/src/main/java/com/google/gerrit/k8s/operator/network/model/GerritNetwork.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/model/GerritNetwork.java
new file mode 100644
index 0000000..c6e37dd
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/network/model/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.network.model;
+
+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("v1alpha1")
+@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/operator/src/main/java/com/google/gerrit/k8s/operator/network/model/GerritNetworkSpec.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/model/GerritNetworkSpec.java
new file mode 100644
index 0000000..1bfd222
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/network/model/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.network.model;
+
+import com.google.gerrit.k8s.operator.shared.model.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/operator/src/main/java/com/google/gerrit/k8s/operator/network/model/NetworkMember.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/model/NetworkMember.java
new file mode 100644
index 0000000..d5a2fd6
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/network/model/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.network.model;
+
+import com.google.gerrit.k8s.operator.shared.model.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/operator/src/main/java/com/google/gerrit/k8s/operator/network/model/NetworkMemberWithSsh.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/model/NetworkMemberWithSsh.java
new file mode 100644
index 0000000..382c4f6
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/network/model/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.network.model;
+
+import com.google.gerrit.k8s.operator.shared.model.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/operator/src/main/java/com/google/gerrit/k8s/operator/network/none/GerritNoIngressReconciler.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/none/GerritNoIngressReconciler.java
new file mode 100644
index 0000000..c184fe9
--- /dev/null
+++ b/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.network.model.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/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/dependent/ReceiverDeployment.java b/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/dependent/ReceiverDeployment.java
index 8215fef..0f2a294 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/dependent/ReceiverDeployment.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/dependent/ReceiverDeployment.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.k8s.operator.receiver.dependent;
 
 import com.google.gerrit.k8s.operator.cluster.model.GerritCluster;
-import com.google.gerrit.k8s.operator.gerrit.GerritReconciler;
+import com.google.gerrit.k8s.operator.receiver.ReceiverReconciler;
 import com.google.gerrit.k8s.operator.receiver.model.Receiver;
 import com.google.gerrit.k8s.operator.shared.model.NfsWorkaroundConfig;
 import io.fabric8.kubernetes.api.model.Container;
@@ -133,13 +133,14 @@
     return GerritCluster.getLabels(
         receiver.getMetadata().getName(),
         getComponentName(receiver),
-        GerritReconciler.class.getSimpleName());
+        ReceiverReconciler.class.getSimpleName());
   }
 
   private Set<Volume> getVolumes(Receiver receiver) {
     Set<Volume> volumes = new HashSet<>();
-    volumes.add(GerritCluster.getGitRepositoriesVolume());
-    volumes.add(GerritCluster.getLogsVolume());
+    volumes.add(
+        GerritCluster.getSharedVolume(
+            receiver.getSpec().getStorage().getSharedStorage().getExternalPVC()));
 
     volumes.add(
         new VolumeBuilder()
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/dependent/ReceiverService.java b/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/dependent/ReceiverService.java
index e5d0cb3..307be1a 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/dependent/ReceiverService.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/dependent/ReceiverService.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.k8s.operator.cluster.model.GerritCluster;
 import com.google.gerrit.k8s.operator.receiver.ReceiverReconciler;
 import com.google.gerrit.k8s.operator.receiver.model.Receiver;
-import com.google.gerrit.k8s.operator.receiver.model.ReceiverTemplate;
 import io.fabric8.kubernetes.api.model.Service;
 import io.fabric8.kubernetes.api.model.ServiceBuilder;
 import io.fabric8.kubernetes.api.model.ServicePort;
@@ -60,8 +59,8 @@
     return receiver.getMetadata().getName();
   }
 
-  public static String getName(ReceiverTemplate receiver) {
-    return receiver.getMetadata().getName();
+  public static String getName(String receiverName) {
+    return receiverName;
   }
 
   public static Map<String, String> getLabels(Receiver receiver) {
@@ -72,8 +71,11 @@
   }
 
   public static String getHostname(Receiver receiver) {
-    return String.format(
-        "%s.%s.svc.cluster.local", getName(receiver), receiver.getMetadata().getNamespace());
+    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) {
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/model/Receiver.java b/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/model/Receiver.java
index e23e217..ffda566 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/model/Receiver.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/model/Receiver.java
@@ -23,7 +23,7 @@
 import org.apache.commons.lang3.builder.ToStringStyle;
 
 @Group("gerritoperator.google.com")
-@Version("v1alpha2")
+@Version("v1alpha6")
 @ShortNames("grec")
 public class Receiver extends CustomResource<ReceiverSpec, ReceiverStatus> implements Namespaced {
   private static final long serialVersionUID = 1L;
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/model/ReceiverSpec.java b/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/model/ReceiverSpec.java
index bd2d8f3..96df251 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/model/ReceiverSpec.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/model/ReceiverSpec.java
@@ -15,12 +15,12 @@
 package com.google.gerrit.k8s.operator.receiver.model;
 
 import com.google.gerrit.k8s.operator.shared.model.ContainerImageConfig;
-import com.google.gerrit.k8s.operator.shared.model.GerritStorageConfig;
 import com.google.gerrit.k8s.operator.shared.model.IngressConfig;
+import com.google.gerrit.k8s.operator.shared.model.StorageConfig;
 
 public class ReceiverSpec extends ReceiverTemplateSpec {
   private ContainerImageConfig containerImages = new ContainerImageConfig();
-  private GerritStorageConfig storage = new GerritStorageConfig();
+  private StorageConfig storage = new StorageConfig();
   private IngressConfig ingress = new IngressConfig();
 
   public ReceiverSpec() {}
@@ -37,11 +37,11 @@
     this.containerImages = containerImages;
   }
 
-  public GerritStorageConfig getStorage() {
+  public StorageConfig getStorage() {
     return storage;
   }
 
-  public void setStorage(GerritStorageConfig storage) {
+  public void setStorage(StorageConfig storage) {
     this.storage = storage;
   }
 
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/model/ReceiverTemplate.java b/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/model/ReceiverTemplate.java
index 6805cd1..5c4332a 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/model/ReceiverTemplate.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/model/ReceiverTemplate.java
@@ -21,6 +21,7 @@
 import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
 import com.google.gerrit.k8s.operator.cluster.model.GerritCluster;
 import com.google.gerrit.k8s.operator.shared.model.IngressConfig;
+import com.google.gerrit.k8s.operator.shared.model.StorageConfig;
 import io.fabric8.kubernetes.api.model.KubernetesResource;
 import io.fabric8.kubernetes.api.model.ObjectMeta;
 import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
@@ -72,10 +73,9 @@
     receiver.setMetadata(getReceiverMetadata(gerritCluster));
     ReceiverSpec receiverSpec = new ReceiverSpec(spec);
     receiverSpec.setContainerImages(gerritCluster.getSpec().getContainerImages());
-    receiverSpec.setStorage(gerritCluster.getSpec().getStorage());
+    receiverSpec.setStorage(new StorageConfig(gerritCluster.getSpec().getStorage()));
     IngressConfig ingressConfig = new IngressConfig();
     ingressConfig.setHost(gerritCluster.getSpec().getIngress().getHost());
-    ingressConfig.setType(gerritCluster.getSpec().getIngress().getType());
     ingressConfig.setTlsEnabled(gerritCluster.getSpec().getIngress().getTls().isEnabled());
     receiverSpec.setIngress(ingressConfig);
     receiver.setSpec(receiverSpec);
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/model/ReceiverTemplateSpec.java b/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/model/ReceiverTemplateSpec.java
index 006d1e1..704067a 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/model/ReceiverTemplateSpec.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/model/ReceiverTemplateSpec.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.k8s.operator.receiver.model;
 
+import com.google.gerrit.k8s.operator.shared.model.HttpServiceConfig;
 import io.fabric8.kubernetes.api.model.Affinity;
 import io.fabric8.kubernetes.api.model.IntOrString;
 import io.fabric8.kubernetes.api.model.ResourceRequirements;
@@ -23,7 +24,7 @@
 import java.util.List;
 
 public class ReceiverTemplateSpec {
-  private List<Toleration> tolerations;
+  private List<Toleration> tolerations = new ArrayList<>();
   private Affinity affinity;
   private List<TopologySpreadConstraint> topologySpreadConstraints = new ArrayList<>();
   private String priorityClassName;
@@ -37,7 +38,7 @@
   private ReceiverProbe readinessProbe = new ReceiverProbe();
   private ReceiverProbe livenessProbe = new ReceiverProbe();
 
-  private ReceiverServiceConfig service = new ReceiverServiceConfig();
+  private HttpServiceConfig service = new HttpServiceConfig();
 
   private String credentialSecretRef;
 
@@ -144,11 +145,11 @@
     this.livenessProbe = livenessProbe;
   }
 
-  public ReceiverServiceConfig getService() {
+  public HttpServiceConfig getService() {
     return service;
   }
 
-  public void setService(ReceiverServiceConfig service) {
+  public void setService(HttpServiceConfig service) {
     this.service = service;
   }
 
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/server/GerritAdmissionWebhook.java b/operator/src/main/java/com/google/gerrit/k8s/operator/server/GerritAdmissionWebhook.java
index ec5e098..5f4dd36 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/server/GerritAdmissionWebhook.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/server/GerritAdmissionWebhook.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.k8s.operator.gerrit.config.GerritConfigBuilder;
 import com.google.gerrit.k8s.operator.gerrit.config.InvalidGerritConfigException;
 import com.google.gerrit.k8s.operator.gerrit.model.Gerrit;
+import com.google.gerrit.k8s.operator.shared.model.GlobalRefDbConfig;
 import com.google.inject.Singleton;
 import io.fabric8.kubernetes.api.model.HasMetadata;
 import io.fabric8.kubernetes.api.model.Status;
@@ -47,6 +48,21 @@
           .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 (missingZookeeperConfig(gerrit)) {
+      return new StatusBuilder()
+          .withCode(HttpServletResponse.SC_BAD_REQUEST)
+          .withMessage("Missing zookeeper configuration (.spec.refdb.zookeeper).")
+          .build();
+    }
+
     return new StatusBuilder().withCode(HttpServletResponse.SC_OK).build();
   }
 
@@ -54,6 +70,16 @@
     new GerritConfigBuilder().forGerrit(gerrit).validate();
   }
 
+  private boolean noRefDbConfiguredForHA(Gerrit gerrit) {
+    return gerrit.getSpec().isHighlyAvailablePrimary()
+        && gerrit.getSpec().getRefdb().getDatabase().equals(GlobalRefDbConfig.RefDatabase.NONE);
+  }
+
+  private boolean missingZookeeperConfig(Gerrit gerrit) {
+    return gerrit.getSpec().getRefdb().getDatabase().equals(GlobalRefDbConfig.RefDatabase.ZOOKEEPER)
+        && gerrit.getSpec().getRefdb().getZookeeper() == null;
+  }
+
   @Override
   public String getName() {
     return "gerrit";
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/model/GerritClusterIngressConfig.java b/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/GerritClusterIngressConfig.java
similarity index 86%
rename from operator/src/main/java/com/google/gerrit/k8s/operator/cluster/model/GerritClusterIngressConfig.java
rename to operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/GerritClusterIngressConfig.java
index 5da60bb..d65fc5e 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/model/GerritClusterIngressConfig.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/GerritClusterIngressConfig.java
@@ -12,17 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.k8s.operator.cluster.model;
+package com.google.gerrit.k8s.operator.shared.model;
 
 import com.fasterxml.jackson.annotation.JsonIgnore;
 import java.util.Map;
 
 public class GerritClusterIngressConfig {
   private boolean enabled = false;
-  private IngressType type = IngressType.NONE;
   private String host;
   private Map<String, String> annotations;
   private GerritIngressTlsConfig tls = new GerritIngressTlsConfig();
+  private GerritIngressSshConfig ssh = new GerritIngressSshConfig();
 
   public boolean isEnabled() {
     return enabled;
@@ -32,14 +32,6 @@
     this.enabled = enabled;
   }
 
-  public IngressType getType() {
-    return type;
-  }
-
-  public void setType(IngressType type) {
-    this.type = type;
-  }
-
   public String getHost() {
     return host;
   }
@@ -64,10 +56,12 @@
     this.tls = tls;
   }
 
-  public enum IngressType {
-    NONE,
-    INGRESS,
-    ISTIO
+  public GerritIngressSshConfig getSsh() {
+    return ssh;
+  }
+
+  public void setSsh(GerritIngressSshConfig ssh) {
+    this.ssh = ssh;
   }
 
   @JsonIgnore
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/OptionalSharedStorage.java b/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/GerritIngressSshConfig.java
similarity index 86%
rename from operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/OptionalSharedStorage.java
rename to operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/GerritIngressSshConfig.java
index b4c7365..62ac188 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/OptionalSharedStorage.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/GerritIngressSshConfig.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2022 The Android Open Source Project
+// 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.
@@ -14,8 +14,7 @@
 
 package com.google.gerrit.k8s.operator.shared.model;
 
-public class OptionalSharedStorage extends SharedStorage {
-
+public class GerritIngressSshConfig {
   private boolean enabled = false;
 
   public boolean isEnabled() {
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/model/GerritIngressTlsConfig.java b/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/GerritIngressTlsConfig.java
similarity index 94%
rename from operator/src/main/java/com/google/gerrit/k8s/operator/cluster/model/GerritIngressTlsConfig.java
rename to operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/GerritIngressTlsConfig.java
index 6284b0a..8a4362a 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/model/GerritIngressTlsConfig.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/GerritIngressTlsConfig.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.k8s.operator.cluster.model;
+package com.google.gerrit.k8s.operator.shared.model;
 
 public class GerritIngressTlsConfig {
 
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/GerritStorageConfig.java b/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/GerritStorageConfig.java
index d90480c..25ca54f 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/GerritStorageConfig.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/GerritStorageConfig.java
@@ -14,42 +14,26 @@
 
 package com.google.gerrit.k8s.operator.shared.model;
 
-public class GerritStorageConfig {
+public class GerritStorageConfig extends StorageConfig {
+  private PluginCacheConfig pluginCache = new PluginCacheConfig();
 
-  private StorageClassConfig storageClasses;
-  private SharedStorage gitRepositoryStorage;
-  private SharedStorage logsStorage;
-  private OptionalSharedStorage pluginCacheStorage = new OptionalSharedStorage();
-
-  public StorageClassConfig getStorageClasses() {
-    return storageClasses;
+  public PluginCacheConfig getPluginCache() {
+    return pluginCache;
   }
 
-  public SharedStorage getGitRepositoryStorage() {
-    return gitRepositoryStorage;
+  public void setPluginCache(PluginCacheConfig pluginCache) {
+    this.pluginCache = pluginCache;
   }
 
-  public void setStorageClasses(StorageClassConfig storageClasses) {
-    this.storageClasses = storageClasses;
-  }
+  public class PluginCacheConfig {
+    private boolean enabled;
 
-  public void setGitRepositoryStorage(SharedStorage gitRepositoryStorage) {
-    this.gitRepositoryStorage = gitRepositoryStorage;
-  }
+    public boolean isEnabled() {
+      return enabled;
+    }
 
-  public SharedStorage getLogsStorage() {
-    return logsStorage;
-  }
-
-  public void setLogsStorage(SharedStorage logsStorage) {
-    this.logsStorage = logsStorage;
-  }
-
-  public OptionalSharedStorage getPluginCacheStorage() {
-    return pluginCacheStorage;
-  }
-
-  public void setPluginCacheStorage(OptionalSharedStorage pluginCacheStorage) {
-    this.pluginCacheStorage = pluginCacheStorage;
+    public void setEnabled(boolean enabled) {
+      this.enabled = enabled;
+    }
   }
 }
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/GlobalRefDbConfig.java b/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/GlobalRefDbConfig.java
new file mode 100644
index 0000000..1945d26
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/GlobalRefDbConfig.java
@@ -0,0 +1,41 @@
+// 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.shared.model;
+
+public class GlobalRefDbConfig {
+  private RefDatabase database = RefDatabase.NONE;
+  private ZookeeperRefDbConfig zookeeper;
+
+  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 enum RefDatabase {
+    NONE,
+    ZOOKEEPER,
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/model/ReceiverServiceConfig.java b/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/HttpServiceConfig.java
similarity index 89%
rename from operator/src/main/java/com/google/gerrit/k8s/operator/receiver/model/ReceiverServiceConfig.java
rename to operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/HttpServiceConfig.java
index 7de5f65..e8eb3ae 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/model/ReceiverServiceConfig.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/HttpServiceConfig.java
@@ -12,11 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.k8s.operator.receiver.model;
+package com.google.gerrit.k8s.operator.shared.model;
 
 import java.io.Serializable;
 
-public class ReceiverServiceConfig implements Serializable {
+public class HttpServiceConfig implements Serializable {
   private static final long serialVersionUID = 1L;
 
   String type = "NodePort";
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/OptionalSharedStorage.java b/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/HttpSshServiceConfig.java
similarity index 69%
copy from operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/OptionalSharedStorage.java
copy to operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/HttpSshServiceConfig.java
index b4c7365..cda6975 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/OptionalSharedStorage.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/HttpSshServiceConfig.java
@@ -14,15 +14,18 @@
 
 package com.google.gerrit.k8s.operator.shared.model;
 
-public class OptionalSharedStorage extends SharedStorage {
+import java.io.Serializable;
 
-  private boolean enabled = false;
+public class HttpSshServiceConfig extends HttpServiceConfig implements Serializable {
+  private static final long serialVersionUID = 1L;
 
-  public boolean isEnabled() {
-    return enabled;
+  int sshPort = 0;
+
+  public int getSshPort() {
+    return sshPort;
   }
 
-  public void setEnabled(boolean enabled) {
-    this.enabled = enabled;
+  public void setSshPort(int sshPort) {
+    this.sshPort = sshPort;
   }
 }
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/IngressConfig.java b/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/IngressConfig.java
index 7b6b0c3..dfbc789 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/IngressConfig.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/IngressConfig.java
@@ -15,19 +15,19 @@
 package com.google.gerrit.k8s.operator.shared.model;
 
 import com.fasterxml.jackson.annotation.JsonIgnore;
-import com.google.gerrit.k8s.operator.cluster.model.GerritClusterIngressConfig.IngressType;
 
 public class IngressConfig {
-  private IngressType type = IngressType.NONE;
+  private boolean enabled;
   private String host;
   private boolean tlsEnabled;
+  private GerritIngressSshConfig ssh = new GerritIngressSshConfig();
 
-  public IngressType getType() {
-    return type;
+  public boolean isEnabled() {
+    return enabled;
   }
 
-  public void setType(IngressType type) {
-    this.type = type;
+  public void setEnabled(boolean enabled) {
+    this.enabled = enabled;
   }
 
   public String getHost() {
@@ -46,6 +46,14 @@
     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());
@@ -57,4 +65,11 @@
     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/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/SharedStorage.java b/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/SharedStorage.java
index 2b5581e..be68fbf 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/SharedStorage.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/SharedStorage.java
@@ -18,11 +18,19 @@
 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;
   }
@@ -46,4 +54,25 @@
   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/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/StorageConfig.java b/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/StorageConfig.java
new file mode 100644
index 0000000..2551c4b
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/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.shared.model;
+
+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/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/ZookeeperRefDbConfig.java b/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/ZookeeperRefDbConfig.java
new file mode 100644
index 0000000..f86ebfd
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/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.shared.model;
+
+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/operator/src/main/resources/META-INF/services/io.fabric8.kubernetes.api.model.KubernetesResource b/operator/src/main/resources/META-INF/services/io.fabric8.kubernetes.api.model.KubernetesResource
index e140914..afd1d50 100644
--- a/operator/src/main/resources/META-INF/services/io.fabric8.kubernetes.api.model.KubernetesResource
+++ b/operator/src/main/resources/META-INF/services/io.fabric8.kubernetes.api.model.KubernetesResource
@@ -1,4 +1,5 @@
 com.google.gerrit.k8s.operator.cluster.model.GerritCluster
 com.google.gerrit.k8s.operator.gerrit.model.Gerrit
 com.google.gerrit.k8s.operator.gitgc.model.GitGarbageCollection
-com.google.gerrit.k8s.operator.receiver.model.Receiver
\ No newline at end of file
+com.google.gerrit.k8s.operator.receiver.model.Receiver
+com.google.gerrit.k8s.operator.network.model.GerritNetwork
\ No newline at end of file
diff --git a/operator/src/test/java/com/google/gerrit/k8s/operator/cluster/GerritClusterE2E.java b/operator/src/test/java/com/google/gerrit/k8s/operator/cluster/GerritClusterE2E.java
index 1427707..b73f463 100644
--- a/operator/src/test/java/com/google/gerrit/k8s/operator/cluster/GerritClusterE2E.java
+++ b/operator/src/test/java/com/google/gerrit/k8s/operator/cluster/GerritClusterE2E.java
@@ -21,9 +21,9 @@
 import static org.hamcrest.Matchers.notNullValue;
 
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.k8s.operator.cluster.dependent.GerritLogsPVC;
-import com.google.gerrit.k8s.operator.cluster.dependent.GitRepositoriesPVC;
 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;
@@ -33,8 +33,8 @@
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   @Test
-  void testGitRepositoriesPvcCreated() {
-    logger.atInfo().log("Waiting max 1 minutes for the git repositories pvc to be created.");
+  void testSharedPvcCreated() {
+    logger.atInfo().log("Waiting max 1 minutes for the shared pvc to be created.");
     await()
         .atMost(1, MINUTES)
         .untilAsserted(
@@ -43,24 +43,7 @@
                   client
                       .persistentVolumeClaims()
                       .inNamespace(operator.getNamespace())
-                      .withName(GitRepositoriesPVC.REPOSITORY_PVC_NAME)
-                      .get();
-              assertThat(pvc, is(notNullValue()));
-            });
-  }
-
-  @Test
-  void testGerritLogsPvcCreated() {
-    logger.atInfo().log("Waiting max 1 minutes for the gerrit logs pvc to be created.");
-    await()
-        .atMost(1, MINUTES)
-        .untilAsserted(
-            () -> {
-              PersistentVolumeClaim pvc =
-                  client
-                      .persistentVolumeClaims()
-                      .inNamespace(operator.getNamespace())
-                      .withName(GerritLogsPVC.LOGS_PVC_NAME)
+                      .withName(SharedPVC.SHARED_PVC_NAME)
                       .get();
               assertThat(pvc, is(notNullValue()));
             });
@@ -83,4 +66,9 @@
               assertThat(cm, is(notNullValue()));
             });
   }
+
+  @Override
+  protected IngressType getIngressType() {
+    return IngressType.INGRESS;
+  }
 }
diff --git a/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/ClusterManagedGerritE2E.java b/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/ClusterManagedGerritWithIngressE2E.java
similarity index 81%
rename from operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/ClusterManagedGerritE2E.java
rename to operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/ClusterManagedGerritWithIngressE2E.java
index 8e92152..b0fd748 100644
--- a/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/ClusterManagedGerritE2E.java
+++ b/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/ClusterManagedGerritWithIngressE2E.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.k8s.operator.gerrit;
 
-import static com.google.gerrit.k8s.operator.cluster.dependent.GerritClusterIngress.INGRESS_NAME;
+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;
@@ -27,9 +27,9 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.GerritApi;
-import com.google.gerrit.k8s.operator.cluster.model.GerritClusterIngressConfig.IngressType;
 import com.google.gerrit.k8s.operator.gerrit.model.GerritTemplate;
 import com.google.gerrit.k8s.operator.gerrit.model.GerritTemplateSpec.GerritMode;
+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 io.fabric8.kubernetes.api.model.networking.v1.Ingress;
@@ -38,12 +38,11 @@
 import java.util.List;
 import org.junit.jupiter.api.Test;
 
-public class ClusterManagedGerritE2E extends AbstractGerritOperatorE2ETest {
+public class ClusterManagedGerritWithIngressE2E extends AbstractGerritOperatorE2ETest {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   @Test
   void testPrimaryGerritIsCreated() throws Exception {
-    gerritCluster.setIngressType(IngressType.INGRESS);
     TestGerrit gerrit =
         new TestGerrit(client, testProps, GerritMode.PRIMARY, "gerrit", operator.getNamespace());
     GerritTemplate gerritTemplate = gerrit.createGerritTemplate();
@@ -71,28 +70,7 @@
               assertThat(lbIngresses.get(0).getIp(), is(notNullValue()));
             });
 
-    GerritApi gerritApi = gerritCluster.getGerritApiClient(gerritTemplate);
-    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 testPrimaryGerritWithIstio() throws Exception {
-    gerritCluster.setIngressType(IngressType.ISTIO);
-    GerritTemplate gerrit =
-        new TestGerrit(client, testProps, GerritMode.PRIMARY, "gerrit", operator.getNamespace())
-            .createGerritTemplate();
-    gerritCluster.addGerrit(gerrit);
-    gerritCluster.deploy();
-
-    GerritApi gerritApi = gerritCluster.getGerritApiClient(gerrit);
+    GerritApi gerritApi = gerritCluster.getGerritApiClient(gerritTemplate, IngressType.INGRESS);
     await()
         .atMost(2, MINUTES)
         .untilAsserted(
@@ -154,4 +132,9 @@
             .getLog()
             .contains("Gerrit Code Review [replica]"));
   }
+
+  @Override
+  protected IngressType getIngressType() {
+    return IngressType.INGRESS;
+  }
 }
diff --git a/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/ClusterManagedGerritE2E.java b/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/ClusterManagedGerritWithIstioE2E.java
similarity index 64%
copy from operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/ClusterManagedGerritE2E.java
copy to operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/ClusterManagedGerritWithIstioE2E.java
index 8e92152..9fe2d05 100644
--- a/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/ClusterManagedGerritE2E.java
+++ b/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/ClusterManagedGerritWithIstioE2E.java
@@ -14,12 +14,10 @@
 
 package com.google.gerrit.k8s.operator.gerrit;
 
-import static com.google.gerrit.k8s.operator.cluster.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;
@@ -27,72 +25,25 @@
 
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.GerritApi;
-import com.google.gerrit.k8s.operator.cluster.model.GerritClusterIngressConfig.IngressType;
 import com.google.gerrit.k8s.operator.gerrit.model.GerritTemplate;
 import com.google.gerrit.k8s.operator.gerrit.model.GerritTemplateSpec.GerritMode;
+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 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 ClusterManagedGerritE2E extends AbstractGerritOperatorE2ETest {
+public class ClusterManagedGerritWithIstioE2E extends AbstractGerritOperatorE2ETest {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   @Test
-  void testPrimaryGerritIsCreated() throws Exception {
-    gerritCluster.setIngressType(IngressType.INGRESS);
-    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);
-    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 testPrimaryGerritWithIstio() throws Exception {
-    gerritCluster.setIngressType(IngressType.ISTIO);
     GerritTemplate gerrit =
         new TestGerrit(client, testProps, GerritMode.PRIMARY, "gerrit", operator.getNamespace())
             .createGerritTemplate();
     gerritCluster.addGerrit(gerrit);
     gerritCluster.deploy();
 
-    GerritApi gerritApi = gerritCluster.getGerritApiClient(gerrit);
+    GerritApi gerritApi = gerritCluster.getGerritApiClient(gerrit, IngressType.ISTIO);
     await()
         .atMost(2, MINUTES)
         .untilAsserted(
@@ -123,6 +74,37 @@
   }
 
   @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 =
@@ -154,4 +136,9 @@
             .getLog()
             .contains("Gerrit Code Review [replica]"));
   }
+
+  @Override
+  protected IngressType getIngressType() {
+    return IngressType.ISTIO;
+  }
 }
diff --git a/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/GerritConfigReconciliationE2E.java b/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/GerritConfigReconciliationE2E.java
index 228e192..cb26331 100644
--- a/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/GerritConfigReconciliationE2E.java
+++ b/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/GerritConfigReconciliationE2E.java
@@ -23,6 +23,8 @@
 import com.google.gerrit.k8s.operator.gerrit.model.GerritTemplate;
 import com.google.gerrit.k8s.operator.gerrit.model.GerritTemplateSpec;
 import com.google.gerrit.k8s.operator.gerrit.model.GerritTemplateSpec.GerritMode;
+import com.google.gerrit.k8s.operator.network.IngressType;
+import com.google.gerrit.k8s.operator.shared.model.HttpSshServiceConfig;
 import com.google.gerrit.k8s.operator.test.AbstractGerritOperatorE2ETest;
 import com.google.gerrit.k8s.operator.test.TestGerrit;
 import java.util.HashMap;
@@ -52,7 +54,9 @@
 
     gerritCluster.removeGerrit(gerritTemplate);
     GerritTemplateSpec gerritSpec = gerritTemplate.getSpec();
-    gerritSpec.setGracefulStopTimeout(10);
+    HttpSshServiceConfig gerritServiceConfig = new HttpSshServiceConfig();
+    gerritServiceConfig.setHttpPort(48080);
+    gerritSpec.setService(gerritServiceConfig);
     gerritTemplate.setSpec(gerritSpec);
     gerritCluster.addGerrit(gerritTemplate);
     gerritCluster.deploy();
@@ -63,25 +67,14 @@
             () -> {
               assertTrue(
                   client
-                          .apps()
-                          .statefulSets()
-                          .inNamespace(operator.getNamespace())
-                          .withName(GERRIT_NAME)
-                          .get()
-                          .getSpec()
-                          .getTemplate()
-                          .getSpec()
-                          .getTerminationGracePeriodSeconds()
-                      == 10L);
-              assertTrue(
-                  client
-                          .pods()
-                          .inNamespace(operator.getNamespace())
-                          .withName(GERRIT_NAME + "-0")
-                          .get()
-                          .getSpec()
-                          .getTerminationGracePeriodSeconds()
-                      == 10L);
+                      .services()
+                      .inNamespace(operator.getNamespace())
+                      .withName(GERRIT_NAME)
+                      .get()
+                      .getSpec()
+                      .getPorts()
+                      .stream()
+                      .allMatch(p -> p.getPort() == 48080));
               assertFalse(getReplicaSetAnnotations().containsKey(RESTART_ANNOTATION));
             });
   }
@@ -107,21 +100,7 @@
     gerritCluster.addGerrit(gerritTemplate);
     gerritCluster.deploy();
 
-    await()
-        .atMost(2, MINUTES)
-        .untilAsserted(
-            () -> {
-              assertTrue(getReplicaSetAnnotations().containsKey(RESTART_ANNOTATION));
-              assertFalse(
-                  podV1Uid.equals(
-                      client
-                          .pods()
-                          .inNamespace(operator.getNamespace())
-                          .withName(GERRIT_NAME + "-0")
-                          .get()
-                          .getMetadata()
-                          .getUid()));
-            });
+    assertGerritRestart(podV1Uid);
   }
 
   @Test
@@ -137,13 +116,23 @@
 
     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(
-                  podV1Uid.equals(
+                  uidOld.equals(
                       client
                           .pods()
                           .inNamespace(operator.getNamespace())
@@ -166,4 +155,9 @@
         .getMetadata()
         .getAnnotations();
   }
+
+  @Override
+  protected IngressType getIngressType() {
+    return IngressType.INGRESS;
+  }
 }
diff --git a/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/StandaloneGerritE2E.java b/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/StandaloneGerritE2E.java
index 375b375..236567d 100644
--- a/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/StandaloneGerritE2E.java
+++ b/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/StandaloneGerritE2E.java
@@ -16,8 +16,8 @@
 
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
-import com.google.gerrit.k8s.operator.cluster.model.GerritClusterIngressConfig.IngressType;
 import com.google.gerrit.k8s.operator.gerrit.model.GerritTemplateSpec.GerritMode;
+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 org.junit.jupiter.api.Test;
@@ -26,8 +26,6 @@
 
   @Test
   void testPrimaryGerritIsCreated() throws Exception {
-    gerritCluster.setIngressType(IngressType.INGRESS);
-
     String gerritName = "gerrit";
     TestGerrit testGerrit = new TestGerrit(client, testProps, gerritName, operator.getNamespace());
     testGerrit.deploy();
@@ -58,4 +56,9 @@
             .getLog()
             .contains("Gerrit Code Review [replica]"));
   }
+
+  @Override
+  protected IngressType getIngressType() {
+    return IngressType.INGRESS;
+  }
 }
diff --git a/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/config/GerritConfigBuilderTest.java b/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/config/GerritConfigBuilderTest.java
index e59b67d..fa0fb31 100644
--- a/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/config/GerritConfigBuilderTest.java
+++ b/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/config/GerritConfigBuilderTest.java
@@ -18,6 +18,9 @@
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import com.google.gerrit.k8s.operator.gerrit.model.Gerrit;
+import com.google.gerrit.k8s.operator.gerrit.model.GerritSpec;
+import java.util.Map;
 import java.util.Set;
 import org.assertj.core.util.Arrays;
 import org.eclipse.jgit.lib.Config;
@@ -27,8 +30,9 @@
 
   @Test
   public void emptyGerritConfigContainsAllPresetConfiguration() {
-    GerritConfigBuilder cfgBuilder = new GerritConfigBuilder();
-    Config cfg = cfgBuilder.withConfig("").build();
+    Gerrit gerrit = createGerrit("");
+    ConfigBuilder cfgBuilder = new GerritConfigBuilder().forGerrit(gerrit);
+    Config cfg = cfgBuilder.build();
     for (RequiredOption<?> opt : cfgBuilder.getRequiredOptions()) {
       if (opt.getExpected() instanceof String || opt.getExpected() instanceof Boolean) {
         assertTrue(
@@ -44,21 +48,29 @@
 
   @Test
   public void invalidConfigValueIsRejected() {
+    Gerrit gerrit = createGerrit("[gerrit]\n  basePath = invalid");
     assertThrows(
-        IllegalStateException.class,
-        () -> new GerritConfigBuilder().withConfig("[gerrit]\n  basePath = invalid").build());
+        IllegalStateException.class, () -> new GerritConfigBuilder().forGerrit(gerrit).build());
   }
 
   @Test
   public void validConfigValueIsAccepted() {
-    assertDoesNotThrow(
-        () -> new GerritConfigBuilder().withConfig("[gerrit]\n  basePath = git").build());
+    Gerrit gerrit = createGerrit("[gerrit]\n  basePath = git");
+    assertDoesNotThrow(() -> new GerritConfigBuilder().forGerrit(gerrit).build());
   }
 
   @Test
   public void canonicalWebUrlIsConfigured() {
     String url = "https://gerrit.example.com";
-    Config cfg = new GerritConfigBuilder().withConfig("").withUrl(url).build();
+    Config cfg = new GerritConfigBuilder().withUrl(url).build();
     assertTrue(cfg.getString("gerrit", null, "canonicalWebUrl").equals(url));
   }
+
+  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/operator/src/test/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollectionE2E.java b/operator/src/test/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollectionE2E.java
index fac930e..3a62cac 100644
--- a/operator/src/test/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollectionE2E.java
+++ b/operator/src/test/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollectionE2E.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.k8s.operator.gitgc.model.GitGarbageCollection;
 import com.google.gerrit.k8s.operator.gitgc.model.GitGarbageCollectionSpec;
 import com.google.gerrit.k8s.operator.gitgc.model.GitGarbageCollectionStatus;
+import com.google.gerrit.k8s.operator.network.IngressType;
 import com.google.gerrit.k8s.operator.test.AbstractGerritOperatorE2ETest;
 import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
 import io.fabric8.kubernetes.api.model.batch.v1.CronJob;
@@ -225,4 +226,9 @@
     assert (jobRuns.size() > 0);
     assert (jobRuns.get(0).getMetadata().getName().startsWith(gitGcName));
   }
+
+  @Override
+  protected IngressType getIngressType() {
+    return IngressType.NONE;
+  }
 }
diff --git a/operator/src/test/java/com/google/gerrit/k8s/operator/network/ingress/dependent/GerritClusterIngressTest.java b/operator/src/test/java/com/google/gerrit/k8s/operator/network/ingress/dependent/GerritClusterIngressTest.java
new file mode 100644
index 0000000..7e854ab
--- /dev/null
+++ b/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.network.model.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/operator/src/test/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritClusterIstioTest.java b/operator/src/test/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritClusterIstioTest.java
new file mode 100644
index 0000000..17b27e2
--- /dev/null
+++ b/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.network.model.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/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/ClusterManagedReceiverE2E.java b/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/AbstractClusterManagedReceiverE2E.java
similarity index 88%
rename from operator/src/test/java/com/google/gerrit/k8s/operator/receiver/ClusterManagedReceiverE2E.java
rename to operator/src/test/java/com/google/gerrit/k8s/operator/receiver/AbstractClusterManagedReceiverE2E.java
index 2036cdf..bde1afe 100644
--- a/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/ClusterManagedReceiverE2E.java
+++ b/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/AbstractClusterManagedReceiverE2E.java
@@ -20,7 +20,6 @@
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import com.google.gerrit.k8s.operator.cluster.model.GerritCluster;
-import com.google.gerrit.k8s.operator.cluster.model.GerritClusterIngressConfig.IngressType;
 import com.google.gerrit.k8s.operator.gerrit.model.GerritTemplate;
 import com.google.gerrit.k8s.operator.gerrit.model.GerritTemplateSpec.GerritMode;
 import com.google.gerrit.k8s.operator.receiver.model.ReceiverTemplate;
@@ -44,7 +43,7 @@
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 
-public class ClusterManagedReceiverE2E extends AbstractGerritOperatorE2ETest {
+public abstract class AbstractClusterManagedReceiverE2E extends AbstractGerritOperatorE2ETest {
   private static final String GERRIT_NAME = "gerrit";
   private ReceiverTemplate receiver;
   private GerritTemplate gerrit;
@@ -62,20 +61,11 @@
     receiverTemplateSpec.setCredentialSecretRef(ReceiverUtil.CREDENTIALS_SECRET_NAME);
     receiver.setSpec(receiverTemplateSpec);
     gerritCluster.setReceiver(receiver);
-
     gerritCluster.deploy();
   }
 
   @Test
-  public void testProjectLifecycleWithIngress(@TempDir Path tempDir) throws Exception {
-    gerritCluster.setIngressType(IngressType.INGRESS);
-    GerritCluster cluster = gerritCluster.getGerritCluster();
-    assertProjectLifecycle(cluster, tempDir);
-  }
-
-  @Test
-  public void testProjectLifecycleWithIstio(@TempDir Path tempDir) throws Exception {
-    gerritCluster.setIngressType(IngressType.ISTIO);
+  public void testProjectLifecycle(@TempDir Path tempDir) throws Exception {
     GerritCluster cluster = gerritCluster.getGerritCluster();
     assertProjectLifecycle(cluster, tempDir);
   }
@@ -121,7 +111,7 @@
   private URL getGerritUrl(String path) throws Exception {
     return new URIBuilder()
         .setScheme("https")
-        .setHost(gerritCluster.getGerritHostname(gerrit))
+        .setHost(gerritCluster.getHostname())
         .setPath(path)
         .build()
         .toURL();
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/OptionalSharedStorage.java b/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/ClusterManagedReceiverWithIngressE2E.java
similarity index 67%
copy from operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/OptionalSharedStorage.java
copy to operator/src/test/java/com/google/gerrit/k8s/operator/receiver/ClusterManagedReceiverWithIngressE2E.java
index b4c7365..4dd8bd3 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/OptionalSharedStorage.java
+++ b/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/ClusterManagedReceiverWithIngressE2E.java
@@ -12,17 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.k8s.operator.shared.model;
+package com.google.gerrit.k8s.operator.receiver;
 
-public class OptionalSharedStorage extends SharedStorage {
+import com.google.gerrit.k8s.operator.network.IngressType;
 
-  private boolean enabled = false;
+public class ClusterManagedReceiverWithIngressE2E extends AbstractClusterManagedReceiverE2E {
 
-  public boolean isEnabled() {
-    return enabled;
-  }
-
-  public void setEnabled(boolean enabled) {
-    this.enabled = enabled;
+  @Override
+  protected IngressType getIngressType() {
+    return IngressType.INGRESS;
   }
 }
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/OptionalSharedStorage.java b/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/ClusterManagedReceiverWithIstioE2E.java
similarity index 67%
copy from operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/OptionalSharedStorage.java
copy to operator/src/test/java/com/google/gerrit/k8s/operator/receiver/ClusterManagedReceiverWithIstioE2E.java
index b4c7365..e76303c 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/shared/model/OptionalSharedStorage.java
+++ b/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/ClusterManagedReceiverWithIstioE2E.java
@@ -12,17 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.k8s.operator.shared.model;
+package com.google.gerrit.k8s.operator.receiver;
 
-public class OptionalSharedStorage extends SharedStorage {
+import com.google.gerrit.k8s.operator.network.IngressType;
 
-  private boolean enabled = false;
-
-  public boolean isEnabled() {
-    return enabled;
-  }
-
-  public void setEnabled(boolean enabled) {
-    this.enabled = enabled;
+public class ClusterManagedReceiverWithIstioE2E extends AbstractClusterManagedReceiverE2E {
+  @Override
+  protected IngressType getIngressType() {
+    return IngressType.ISTIO;
   }
 }
diff --git a/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/dependent/ReceiverTest.java b/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/dependent/ReceiverTest.java
new file mode 100644
index 0000000..488412d
--- /dev/null
+++ b/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.receiver.model.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/operator/src/test/java/com/google/gerrit/k8s/operator/server/GerritAdmissionWebhookTest.java b/operator/src/test/java/com/google/gerrit/k8s/operator/server/GerritAdmissionWebhookTest.java
index 86f25da..be50752 100644
--- a/operator/src/test/java/com/google/gerrit/k8s/operator/server/GerritAdmissionWebhookTest.java
+++ b/operator/src/test/java/com/google/gerrit/k8s/operator/server/GerritAdmissionWebhookTest.java
@@ -20,12 +20,12 @@
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.gerrit.k8s.operator.cluster.model.GerritCluster;
-import com.google.gerrit.k8s.operator.cluster.model.GerritClusterIngressConfig;
 import com.google.gerrit.k8s.operator.cluster.model.GerritClusterSpec;
 import com.google.gerrit.k8s.operator.gerrit.model.Gerrit;
 import com.google.gerrit.k8s.operator.gerrit.model.GerritSpec;
 import com.google.gerrit.k8s.operator.gerrit.model.GerritTemplateSpec.GerritMode;
 import com.google.gerrit.k8s.operator.receiver.model.Receiver;
+import com.google.gerrit.k8s.operator.shared.model.GerritClusterIngressConfig;
 import com.google.gerrit.k8s.operator.test.TestAdmissionWebhookServer;
 import io.fabric8.kubernetes.api.model.DefaultKubernetesResourceList;
 import io.fabric8.kubernetes.api.model.HasMetadata;
@@ -125,6 +125,7 @@
     GerritClusterIngressConfig ingressConfig = new GerritClusterIngressConfig();
     ingressConfig.setEnabled(false);
     clusterSpec.setIngress(ingressConfig);
+    clusterSpec.setServerId("test");
     cluster.setSpec(clusterSpec);
 
     kubernetesServer
diff --git a/operator/src/test/java/com/google/gerrit/k8s/operator/test/AbstractGerritOperatorE2ETest.java b/operator/src/test/java/com/google/gerrit/k8s/operator/test/AbstractGerritOperatorE2ETest.java
index d76664b..bc8780f 100644
--- a/operator/src/test/java/com/google/gerrit/k8s/operator/test/AbstractGerritOperatorE2ETest.java
+++ b/operator/src/test/java/com/google/gerrit/k8s/operator/test/AbstractGerritOperatorE2ETest.java
@@ -21,6 +21,9 @@
 import com.google.gerrit.k8s.operator.gerrit.model.Gerrit;
 import com.google.gerrit.k8s.operator.gitgc.GitGarbageCollectionReconciler;
 import com.google.gerrit.k8s.operator.gitgc.model.GitGarbageCollection;
+import com.google.gerrit.k8s.operator.network.GerritNetworkReconcilerProvider;
+import com.google.gerrit.k8s.operator.network.IngressType;
+import com.google.gerrit.k8s.operator.network.model.GerritNetwork;
 import com.google.gerrit.k8s.operator.receiver.ReceiverReconciler;
 import com.google.gerrit.k8s.operator.receiver.model.Receiver;
 import io.fabric8.kubernetes.api.model.Secret;
@@ -28,6 +31,7 @@
 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;
@@ -39,7 +43,7 @@
 import org.junit.jupiter.api.extension.RegisterExtension;
 import org.mockito.Mockito;
 
-public class AbstractGerritOperatorE2ETest {
+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";
@@ -53,11 +57,13 @@
   @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
@@ -73,6 +79,7 @@
     client.resource(receiverCredentials).inNamespace(operator.getNamespace()).createOrReplace();
 
     gerritCluster = new TestGerritCluster(client, operator.getNamespace());
+    gerritCluster.setIngressType(getIngressType());
     gerritCluster.deploy();
   }
 
@@ -124,4 +131,10 @@
             .build();
     client.resource(imagePullSecret).createOrReplace();
   }
+
+  public Reconciler<GerritNetwork> getGerritNetworkReconciler() {
+    return new GerritNetworkReconcilerProvider(getIngressType()).get();
+  }
+
+  protected abstract IngressType getIngressType();
 }
diff --git a/operator/src/test/java/com/google/gerrit/k8s/operator/test/ReceiverUtil.java b/operator/src/test/java/com/google/gerrit/k8s/operator/test/ReceiverUtil.java
index 3ac188f..165940c 100644
--- a/operator/src/test/java/com/google/gerrit/k8s/operator/test/ReceiverUtil.java
+++ b/operator/src/test/java/com/google/gerrit/k8s/operator/test/ReceiverUtil.java
@@ -54,12 +54,7 @@
   public static URL getReceiverUrl(GerritCluster gerritCluster, String path) throws Exception {
     return new URIBuilder()
         .setScheme("https")
-        .setHost(
-            gerritCluster
-                .getSpec()
-                .getIngress()
-                .getFullHostnameForService(
-                    gerritCluster.getSpec().getReceiver().getMetadata().getName()))
+        .setHost(gerritCluster.getSpec().getIngress().getHost())
         .setPath(path)
         .build()
         .toURL();
diff --git a/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestGerrit.java b/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestGerrit.java
index 57794e1..f3f9546 100644
--- a/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestGerrit.java
+++ b/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestGerrit.java
@@ -23,7 +23,6 @@
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.k8s.operator.cluster.model.GerritClusterIngressConfig.IngressType;
 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;
@@ -55,9 +54,7 @@
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
   public static final TestProperties testProps = new TestProperties();
   public static final String DEFAULT_GERRIT_CONFIG =
-      "[gerrit]\n"
-          + "  serverId = gerrit-1\n"
-          + "[index]\n"
+      "[index]\n"
           + "  type = LUCENE\n"
           + "[auth]\n"
           + "  type = LDAP\n"
@@ -177,6 +174,7 @@
       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")))
@@ -186,18 +184,14 @@
     gerritSpec.setConfigFiles(Map.of("gerrit.config", config.toText()));
     gerritSpec.setSecretRef(SECURE_CONFIG_SECRET_NAME);
 
-    SharedStorage repoStorage = new SharedStorage();
-    repoStorage.setSize(Quantity.parse("1Gi"));
-
-    SharedStorage logStorage = new SharedStorage();
-    logStorage.setSize(Quantity.parse("1Gi"));
+    SharedStorage sharedStorage = new SharedStorage();
+    sharedStorage.setSize(Quantity.parse("1Gi"));
 
     StorageClassConfig storageClassConfig = new StorageClassConfig();
     storageClassConfig.setReadWriteMany(testProps.getRWMStorageClass());
 
     GerritStorageConfig gerritStorageConfig = new GerritStorageConfig();
-    gerritStorageConfig.setGitRepositoryStorage(repoStorage);
-    gerritStorageConfig.setLogsStorage(logStorage);
+    gerritStorageConfig.setSharedStorage(sharedStorage);
     gerritStorageConfig.setStorageClasses(storageClassConfig);
     gerritSpec.setStorage(gerritStorageConfig);
 
@@ -216,7 +210,6 @@
 
     IngressConfig ingressConfig = new IngressConfig();
     ingressConfig.setHost(testProps.getIngressDomain());
-    ingressConfig.setType(IngressType.INGRESS);
     ingressConfig.setTlsEnabled(false);
     gerritSpec.setIngress(ingressConfig);
 
diff --git a/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestGerritCluster.java b/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestGerritCluster.java
index 2261fe0..99598f9 100644
--- a/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestGerritCluster.java
+++ b/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestGerritCluster.java
@@ -15,6 +15,7 @@
 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;
@@ -25,13 +26,13 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.extensions.api.GerritApi;
 import com.google.gerrit.k8s.operator.cluster.model.GerritCluster;
-import com.google.gerrit.k8s.operator.cluster.model.GerritClusterIngressConfig;
-import com.google.gerrit.k8s.operator.cluster.model.GerritClusterIngressConfig.IngressType;
 import com.google.gerrit.k8s.operator.cluster.model.GerritClusterSpec;
-import com.google.gerrit.k8s.operator.cluster.model.GerritIngressTlsConfig;
 import com.google.gerrit.k8s.operator.gerrit.model.GerritTemplate;
+import com.google.gerrit.k8s.operator.network.IngressType;
 import com.google.gerrit.k8s.operator.receiver.model.ReceiverTemplate;
 import com.google.gerrit.k8s.operator.shared.model.ContainerImageConfig;
+import com.google.gerrit.k8s.operator.shared.model.GerritClusterIngressConfig;
+import com.google.gerrit.k8s.operator.shared.model.GerritIngressTlsConfig;
 import com.google.gerrit.k8s.operator.shared.model.GerritRepositoryConfig;
 import com.google.gerrit.k8s.operator.shared.model.GerritStorageConfig;
 import com.google.gerrit.k8s.operator.shared.model.NfsWorkaroundConfig;
@@ -46,7 +47,6 @@
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 
@@ -80,20 +80,6 @@
     return hostname;
   }
 
-  public String getGerritHostname(GerritTemplate gerrit) {
-    switch (cluster.getSpec().getIngress().getType()) {
-      case ISTIO:
-        return hostname;
-      case INGRESS:
-        return cluster
-            .getSpec()
-            .getIngress()
-            .getFullHostnameForService(gerrit.getMetadata().getName());
-      default:
-        throw new IllegalStateException("No ingress configured.");
-    }
-  }
-
   public String getNamespace() {
     return cluster.getMetadata().getNamespace();
   }
@@ -101,52 +87,38 @@
   public void setIngressType(IngressType type) {
     switch (type) {
       case INGRESS:
+        hostname = testProps.getIngressDomain();
         enableIngress();
         break;
       case ISTIO:
-        enableIstio();
+        hostname = testProps.getIstioDomain();
+        enableIngress();
         break;
       default:
+        hostname = null;
         defaultIngressConfig();
     }
     deploy();
   }
 
   private void defaultIngressConfig() {
-    hostname = null;
     ingressConfig = new GerritClusterIngressConfig();
     ingressConfig.setEnabled(false);
   }
 
   private void enableIngress() {
-    hostname = testProps.getIngressDomain();
     ingressConfig = new GerritClusterIngressConfig();
     ingressConfig.setEnabled(true);
-    ingressConfig.setType(IngressType.INGRESS);
     ingressConfig.setHost(hostname);
-    ingressConfig.setAnnotations(Map.of("kubernetes.io/ingress.class", "nginx"));
     GerritIngressTlsConfig ingressTlsConfig = new GerritIngressTlsConfig();
     ingressTlsConfig.setEnabled(true);
     ingressTlsConfig.setSecret("tls-secret");
     ingressConfig.setTls(ingressTlsConfig);
   }
 
-  private void enableIstio() {
-    hostname = testProps.getIstioDomain();
-    ingressConfig = new GerritClusterIngressConfig();
-    ingressConfig.setEnabled(true);
-    ingressConfig.setType(IngressType.ISTIO);
-    ingressConfig.setHost(hostname);
-    ingressConfig.setAnnotations(Map.of("kubernetes.io/ingress.class", "nginx"));
-    GerritIngressTlsConfig ingressTlsConfig = new GerritIngressTlsConfig();
-    ingressTlsConfig.setEnabled(true);
-    ingressTlsConfig.setSecret("tls-secret");
-    ingressConfig.setTls(ingressTlsConfig);
-  }
-
-  public GerritApi getGerritApiClient(GerritTemplate gerrit) {
+  public GerritApi getGerritApiClient(GerritTemplate gerrit, IngressType ingressType) {
     return new GerritRestApiFactory()
-        .create(new GerritAuthData.Basic(String.format("https://%s", getGerritHostname(gerrit))));
+        .create(new GerritAuthData.Basic(String.format("https://%s", hostname)));
   }
 
   public void setNfsEnabled(boolean isNfsEnabled) {
@@ -170,11 +142,8 @@
     cluster.setMetadata(
         new ObjectMetaBuilder().withName(CLUSTER_NAME).withNamespace(namespace).build());
 
-    SharedStorage repoStorage = new SharedStorage();
-    repoStorage.setSize(Quantity.parse("1Gi"));
-
-    SharedStorage logStorage = new SharedStorage();
-    logStorage.setSize(Quantity.parse("1Gi"));
+    SharedStorage sharedStorage = new SharedStorage();
+    sharedStorage.setSize(Quantity.parse("1Gi"));
 
     StorageClassConfig storageClassConfig = new StorageClassConfig();
     storageClassConfig.setReadWriteMany(testProps.getRWMStorageClass());
@@ -186,8 +155,7 @@
 
     GerritClusterSpec clusterSpec = new GerritClusterSpec();
     GerritStorageConfig gerritStorageConfig = new GerritStorageConfig();
-    gerritStorageConfig.setGitRepositoryStorage(repoStorage);
-    gerritStorageConfig.setLogsStorage(logStorage);
+    gerritStorageConfig.setSharedStorage(sharedStorage);
     gerritStorageConfig.setStorageClasses(storageClassConfig);
     clusterSpec.setStorage(gerritStorageConfig);
 
@@ -244,6 +212,7 @@
   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(
             () -> {
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary.yaml
new file mode 100644
index 0000000..c53a758
--- /dev/null
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary.yaml
@@ -0,0 +1,15 @@
+apiVersion: "gerritoperator.google.com/v1alpha1"
+kind: GerritNetwork
+metadata:
+  name: gerrit
+  namespace: gerrit
+spec:
+  ingress:
+    enabled: true
+    host: example.com
+    tls:
+      enabled: false
+  primaryGerrit:
+    name: primary
+    httpPort: 48080
+    sshPort: 49418
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica.yaml
new file mode 100644
index 0000000..080fde3
--- /dev/null
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica.yaml
@@ -0,0 +1,19 @@
+apiVersion: "gerritoperator.google.com/v1alpha1"
+kind: GerritNetwork
+metadata:
+  name: gerrit
+  namespace: gerrit
+spec:
+  ingress:
+    enabled: true
+    host: example.com
+    tls:
+      enabled: false
+  primaryGerrit:
+    name: primary
+    httpPort: 48080
+    sshPort: 49418
+  gerritReplica:
+    name: replica
+    httpPort: 48080
+    sshPort: 49418
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica_ssh.yaml b/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/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/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica_tls.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica_tls.yaml
new file mode 100644
index 0000000..ef52342
--- /dev/null
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica_tls.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: true
+      secret: tls-secret
+  primaryGerrit:
+    name: primary
+    httpPort: 48080
+    sshPort: 49418
+  gerritReplica:
+    name: replica
+    httpPort: 48080
+    sshPort: 49418
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_ssh.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_ssh.yaml
new file mode 100644
index 0000000..a6b3271
--- /dev/null
+++ b/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/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_receiver_replica.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_receiver_replica.yaml
new file mode 100644
index 0000000..bce3e4a
--- /dev/null
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_receiver_replica.yaml
@@ -0,0 +1,18 @@
+apiVersion: "gerritoperator.google.com/v1alpha1"
+kind: GerritNetwork
+metadata:
+  name: gerrit
+  namespace: gerrit
+spec:
+  ingress:
+    enabled: true
+    host: example.com
+    tls:
+      enabled: false
+  gerritReplica:
+    name: replica
+    httpPort: 48080
+    sshPort: 49418
+  receiver:
+    name: receiver
+    httpPort: 48081
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_receiver_replica_ssh.yaml b/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/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/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_receiver_replica_tls.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_receiver_replica_tls.yaml
new file mode 100644
index 0000000..a98e9e1
--- /dev/null
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_receiver_replica_tls.yaml
@@ -0,0 +1,19 @@
+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
+  gerritReplica:
+    name: replica
+    httpPort: 48080
+    sshPort: 49418
+  receiver:
+    name: receiver
+    httpPort: 48081
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_replica.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_replica.yaml
new file mode 100644
index 0000000..e3bf64d
--- /dev/null
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_replica.yaml
@@ -0,0 +1,15 @@
+apiVersion: "gerritoperator.google.com/v1alpha1"
+kind: GerritNetwork
+metadata:
+  name: gerrit
+  namespace: gerrit
+spec:
+  ingress:
+    enabled: true
+    host: example.com
+    tls:
+      enabled: false
+  gerritReplica:
+    name: replica
+    httpPort: 48080
+    sshPort: 49418
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_replica_ssh.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_replica_ssh.yaml
new file mode 100644
index 0000000..efa54a7
--- /dev/null
+++ b/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/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_primary.yaml b/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/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/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_primary_replica.yaml b/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/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/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_primary_replica_tls.yaml b/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/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/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_receiver_replica.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_receiver_replica.yaml
new file mode 100644
index 0000000..e123b27
--- /dev/null
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_receiver_replica.yaml
@@ -0,0 +1,46 @@
+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: "/a/projects"
+        backend:
+          service:
+            name: receiver
+            port:
+              name: http
+      - pathType: Prefix
+        path: "/new"
+        backend:
+          service:
+            name: receiver
+            port:
+              name: http
+      - pathType: Prefix
+        path: "/git"
+        backend:
+          service:
+            name: receiver
+            port:
+              name: http
+      - pathType: Prefix
+        path: "/"
+        backend:
+          service:
+            name: replica
+            port:
+              name: http
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_receiver_replica_tls.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_receiver_replica_tls.yaml
new file mode 100644
index 0000000..51dd65c
--- /dev/null
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_receiver_replica_tls.yaml
@@ -0,0 +1,50 @@
+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:
+  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: "/new"
+        backend:
+          service:
+            name: receiver
+            port:
+              name: http
+      - pathType: Prefix
+        path: "/git"
+        backend:
+          service:
+            name: receiver
+            port:
+              name: http
+      - pathType: Prefix
+        path: "/"
+        backend:
+          service:
+            name: replica
+            port:
+              name: http
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_replica.yaml b/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/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/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway.yaml
new file mode 100644
index 0000000..ccbab63
--- /dev/null
+++ b/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/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_primary_replica_ssh.yaml b/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/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/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_primary_ssh.yaml b/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/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/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_receiver_replica_ssh.yaml b/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/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/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_replica_ssh.yaml b/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/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/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_tls.yaml b/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/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/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_primary.yaml b/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/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/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_primary_replica.yaml b/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/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/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_primary_replica_ssh.yaml b/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/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/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_primary_ssh.yaml b/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/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/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_receiver_replica.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_receiver_replica.yaml
new file mode 100644
index 0000000..150c4f3
--- /dev/null
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_receiver_replica.yaml
@@ -0,0 +1,30 @@
+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: "/git/"
+      - uri:
+          prefix: "/a/projects/"
+      - uri:
+          prefix: "/new/"
+    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/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_receiver_replica_ssh.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_receiver_replica_ssh.yaml
new file mode 100644
index 0000000..1898b58
--- /dev/null
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_receiver_replica_ssh.yaml
@@ -0,0 +1,38 @@
+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: "/git/"
+      - uri:
+          prefix: "/a/projects/"
+      - uri:
+          prefix: "/new/"
+    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/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_replica.yaml b/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/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/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_replica_ssh.yaml b/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/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/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/dependent/deployment.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/dependent/deployment.yaml
new file mode 100644
index 0000000..f7f2826
--- /dev/null
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/dependent/deployment.yaml
@@ -0,0 +1,120 @@
+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:
+          httpGet:
+            path: /
+            port: 80
+          initialDelaySeconds: 0
+          periodSeconds: 10
+          timeoutSeconds: 1
+          successThreshold: 1
+          failureThreshold: 3
+
+        livenessProbe:
+          httpGet:
+            path: /
+            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/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/dependent/deployment_minimal.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/dependent/deployment_minimal.yaml
new file mode 100644
index 0000000..26e56ad
--- /dev/null
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/dependent/deployment_minimal.yaml
@@ -0,0 +1,81 @@
+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:
+          httpGet:
+            path: /
+            port: 80
+
+        livenessProbe:
+          httpGet:
+            path: /
+            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/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/dependent/service.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/dependent/service.yaml
new file mode 100644
index 0000000..907ee1e
--- /dev/null
+++ b/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/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/receiver.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/receiver.yaml
new file mode 100644
index 0000000..3364ba0
--- /dev/null
+++ b/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/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/receiver_minimal.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/receiver_minimal.yaml
new file mode 100644
index 0000000..8fdf155
--- /dev/null
+++ b/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/tests/fixtures/helm/gerrit.py b/tests/fixtures/helm/gerrit.py
index 7122e36..ec7a7c1 100644
--- a/tests/fixtures/helm/gerrit.py
+++ b/tests/fixtures/helm/gerrit.py
@@ -162,11 +162,9 @@
         options.add_argument("--headless")
         options.add_argument("--no-sandbox")
         options.add_argument("--ignore-certificate-errors")
-        capabilities = webdriver.DesiredCapabilities.CHROME.copy()
-        capabilities["acceptInsecureCerts"] = True
+        options.set_capability("acceptInsecureCerts", True)
         driver = webdriver.Chrome(
-            chrome_options=options,
-            desired_capabilities=capabilities,
+            options=options,
         )
         driver.get(f"http://{self.hostname}/login")
         user_input = driver.find_element(By.ID, "f_user")
diff --git a/tests/helm-charts/gerrit/test_chart_gerrit_plugins.py b/tests/helm-charts/gerrit/test_chart_gerrit_plugins.py
index 9a9e000..62981ac 100644
--- a/tests/helm-charts/gerrit/test_chart_gerrit_plugins.py
+++ b/tests/helm-charts/gerrit/test_chart_gerrit_plugins.py
@@ -16,14 +16,17 @@
 
 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"
 
 
@@ -43,13 +46,32 @@
     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=[["replication"], ["replication", "download-commands"]],
+    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.plugins.packaged", request.param)
+    gerrit_deployment.set_helm_value("gerrit.pluginManagement.plugins", request.param)
     gerrit_deployment.install()
     gerrit_deployment.create_admin_account()
 
@@ -66,7 +88,9 @@
 ):
     selected_plugins = plugin_list[: request.param]
 
-    gerrit_deployment.set_helm_value("gerrit.plugins.downloaded", selected_plugins)
+    gerrit_deployment.set_helm_value(
+        "gerrit.pluginManagement.plugins", selected_plugins
+    )
 
     gerrit_deployment.install()
     gerrit_deployment.create_admin_account()
@@ -75,10 +99,24 @@
 
 
 @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.plugins.downloaded", [plugin])
+    gerrit_deployment.set_helm_value("gerrit.pluginManagement.plugins", [plugin])
 
     gerrit_deployment.install(wait=False)
 
@@ -94,6 +132,21 @@
     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
@@ -101,8 +154,9 @@
 class TestgerritChartPackagedPluginInstall:
     def _assert_installed_plugins(self, expected_plugins, installed_plugins):
         for plugin in expected_plugins:
-            assert plugin in installed_plugins
-            assert installed_plugins[plugin]["filename"] == f"{plugin}.jar"
+            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(
@@ -133,7 +187,9 @@
         gerrit_deployment, expected_plugins = gerrit_deployment_with_packaged_plugins
         removed_plugin = expected_plugins.pop()
 
-        gerrit_deployment.set_helm_value("gerrit.plugins.packaged", expected_plugins)
+        gerrit_deployment.set_helm_value(
+            "gerrit.pluginManagement.plugins", expected_plugins
+        )
         gerrit_deployment.update()
 
         response = None
@@ -144,12 +200,12 @@
                     "gerrit-admin",
                     ldap_credentials["gerrit-admin"],
                 )
-                if response is not None and removed_plugin not in response:
+                if response is not None and removed_plugin["name"] not in response:
                     break
             except requests.exceptions.ConnectionError:
                 time.sleep(1)
 
-        assert removed_plugin not in response
+        assert removed_plugin["name"] not in response
         self._assert_installed_plugins(expected_plugins, response)
 
 
@@ -188,7 +244,9 @@
     ):
         gerrit_deployment, installed_plugins = gerrit_deployment_with_other_plugins
         removed_plugin = installed_plugins.pop()
-        gerrit_deployment.set_helm_value("gerrit.plugins.downloaded", installed_plugins)
+        gerrit_deployment.set_helm_value(
+            "gerrit.pluginManagement.plugins", installed_plugins
+        )
         gerrit_deployment.update()
 
         response = None
@@ -208,6 +266,43 @@
         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)