Add Gerrit-Maintenance CR for managing maintenance tasks

A Gerrit site requires to run a number of maintenance tasks
regularly. The strategy so far was to have a separate CR per
task. This would not scale with the growing number of
supported tasks.

This change adds the GerritMaintenance CR that is managed
by the GerritCluster resource. This resource is meant to
provide support for maintenance tasks in a Gerrit site.
At the moment, it only supports Git GC, but further tasks
will be added in future changes.

The GitGarbageCollection CR still exists, but is now
deprecated.

Change-Id: I41fc7fb5d56de0c9618a3b8c1dacab86bae42c72
diff --git a/Documentation/examples/1-gerritcluster.yaml b/Documentation/examples/1-gerritcluster.yaml
index 766bd26..49fe6ad 100644
--- a/Documentation/examples/1-gerritcluster.yaml
+++ b/Documentation/examples/1-gerritcluster.yaml
@@ -1,4 +1,4 @@
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: GerritCluster
 metadata:
   name: gerrit
diff --git a/Documentation/examples/2-gerritcluster-with-replica.yaml b/Documentation/examples/2-gerritcluster-with-replica.yaml
index f0ae52c..6a6e667 100644
--- a/Documentation/examples/2-gerritcluster-with-replica.yaml
+++ b/Documentation/examples/2-gerritcluster-with-replica.yaml
@@ -1,4 +1,4 @@
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: GerritCluster
 metadata:
   name: gerrit
diff --git a/Documentation/examples/3-gerritcluster-istio.yaml b/Documentation/examples/3-gerritcluster-istio.yaml
index 4e2b6bf..1b750b6 100644
--- a/Documentation/examples/3-gerritcluster-istio.yaml
+++ b/Documentation/examples/3-gerritcluster-istio.yaml
@@ -1,4 +1,4 @@
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: GerritCluster
 metadata:
   name: gerrit
diff --git a/Documentation/examples/4-gerritcluster-ha-replica.yaml b/Documentation/examples/4-gerritcluster-ha-replica.yaml
index defc929..f1206d5 100644
--- a/Documentation/examples/4-gerritcluster-ha-replica.yaml
+++ b/Documentation/examples/4-gerritcluster-ha-replica.yaml
@@ -1,4 +1,4 @@
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: GerritCluster
 metadata:
   name: gerrit
diff --git a/Documentation/examples/5-gerritcluster-refdb.yaml b/Documentation/examples/5-gerritcluster-refdb.yaml
index c10f84a..1fb64c3 100644
--- a/Documentation/examples/5-gerritcluster-refdb.yaml
+++ b/Documentation/examples/5-gerritcluster-refdb.yaml
@@ -1,4 +1,4 @@
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: GerritCluster
 metadata:
   name: gerrit
diff --git a/Documentation/examples/6-gerritcluster-ha-primary.yaml b/Documentation/examples/6-gerritcluster-ha-primary.yaml
index d3f0cca..10a6b2e 100644
--- a/Documentation/examples/6-gerritcluster-ha-primary.yaml
+++ b/Documentation/examples/6-gerritcluster-ha-primary.yaml
@@ -1,4 +1,4 @@
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: GerritCluster
 metadata:
   name: gerrit
diff --git a/Documentation/examples/gerritcluster-3-nodes-pr-kafka-multisite.yaml b/Documentation/examples/gerritcluster-3-nodes-pr-kafka-multisite.yaml
index cac2217..0f09853 100644
--- a/Documentation/examples/gerritcluster-3-nodes-pr-kafka-multisite.yaml
+++ b/Documentation/examples/gerritcluster-3-nodes-pr-kafka-multisite.yaml
@@ -16,7 +16,7 @@
 type: Opaque
 
 ---
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: GerritCluster
 metadata:
   name: gerrit
diff --git a/Documentation/operator-api-reference.md b/Documentation/operator-api-reference.md
index d7ce28f..5f57dd7 100644
--- a/Documentation/operator-api-reference.md
+++ b/Documentation/operator-api-reference.md
@@ -86,7 +86,7 @@
 ---
 
 **Group**: gerritoperator.google.com \
-**Version**: v1beta13 \
+**Version**: v1beta14 \
 **Kind**: GerritCluster
 
 ---
@@ -103,7 +103,7 @@
 Example:
 
 ```yaml
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: GerritCluster
 metadata:
   name: gerrit
@@ -419,7 +419,7 @@
 ---
 
 **Group**: gerritoperator.google.com \
-**Version**: v1beta13 \
+**Version**: v1beta14 \
 **Kind**: Gerrit
 
 ---
@@ -436,7 +436,7 @@
 Example:
 
 ```yaml
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: Gerrit
 metadata:
   name: gerrit
@@ -645,7 +645,7 @@
 ---
 
 **Group**: gerritoperator.google.com \
-**Version**: v1beta13 \
+**Version**: v1beta14 \
 **Kind**: Receiver
 
 ---
@@ -662,7 +662,7 @@
 Example:
 
 ```yaml
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: Receiver
 metadata:
   name: receiver
@@ -773,7 +773,7 @@
 ---
 
 **Group**: gerritoperator.google.com \
-**Version**: v1beta13 \
+**Version**: v1beta14 \
 **Kind**: GitGarbageCollection
 
 ---
@@ -790,7 +790,7 @@
 Example:
 
 ```yaml
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: GitGarbageCollection
 metadata:
   name: gitgc
@@ -834,7 +834,7 @@
 ---
 
 **Group**: gerritoperator.google.com \
-**Version**: v1beta13 \
+**Version**: v1beta14 \
 **Kind**: GerritNetwork
 
 ---
@@ -850,7 +850,7 @@
 Example:
 
 ```yaml
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: GerritNetwork
 metadata:
   name: gerrit-network
@@ -883,7 +883,7 @@
 ---
 
 **Group**: gerritoperator.google.com \
-**Version**: v1beta13 \
+**Version**: v1beta14 \
 **Kind**: IncomingReplicationTask
 
 ---
@@ -899,7 +899,7 @@
 Example:
 
 ```yaml
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: IncomingReplicationTask
 metadata:
   name: incoming-repl-task
@@ -958,7 +958,7 @@
 ---
 
 **Group**: gerritoperator.google.com \
-**Version**: v1beta13 \
+**Version**: v1beta14 \
 **Kind**: GerritIndexer
 
 ---
@@ -974,7 +974,7 @@
 Example:
 
 ```yaml
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: GerritIndexer
 metadata:
   name: gerrit-indexer
diff --git a/container-images/gerrit-maintenance/Dockerfile b/container-images/gerrit-maintenance/Dockerfile
new file mode 100644
index 0000000..33ff1f2
--- /dev/null
+++ b/container-images/gerrit-maintenance/Dockerfile
@@ -0,0 +1,23 @@
+ARG TAG=latest
+FROM base:${TAG}
+
+RUN apk update && \
+    apk add --no-cache \
+      python3
+
+RUN git clone -b master --depth 1 https://gerrit.googlesource.com/gerrit && \
+    cd gerrit && \
+    git checkout origin/master && \
+    cp -R contrib/maintenance /var/tools && \
+    cd .. && \
+    rm -rf gerrit
+
+RUN mkdir -p /var/gerrit/etc && \
+    chown -R gerrit /var/gerrit && \
+    git config -f /var/gerrit/etc/gerrit.config gerrit.basePath /var/gerrit/git
+
+USER gerrit
+
+VOLUME ["/var/gerrit/git"]
+
+ENTRYPOINT ["/var/tools/gerrit-maintenance.py"]
diff --git a/container-images/publish_list b/container-images/publish_list
index 271c2a5..71407a5 100644
--- a/container-images/publish_list
+++ b/container-images/publish_list
@@ -16,6 +16,7 @@
   echo  "apache-git-http-backend" \
         "gerrit-indexer" \
         "gerrit-init" \
+        "gerrit-maintenance" \
         "gerrit" \
         "git-gc" \
         "fetch-job"
diff --git a/crd/current/gerritclusters.gerritoperator.google.com-v1.yml b/crd/current/gerritclusters.gerritoperator.google.com-v1.yml
index 535a39c..1ab6e4c 100644
--- a/crd/current/gerritclusters.gerritoperator.google.com-v1.yml
+++ b/crd/current/gerritclusters.gerritoperator.google.com-v1.yml
@@ -13,7 +13,7 @@
     singular: gerritcluster
   scope: Namespaced
   versions:
-  - name: v1beta13
+  - name: v1beta14
     schema:
       openAPIV3Schema:
         properties:
@@ -1587,6 +1587,402 @@
                 type: object
               scheduledTasks:
                 properties:
+                  gerritMaintenance:
+                    properties:
+                      projects:
+                        properties:
+                          gc:
+                            items:
+                              properties:
+                                affinity:
+                                  properties:
+                                    nodeAffinity:
+                                      properties:
+                                        preferredDuringSchedulingIgnoredDuringExecution:
+                                          items:
+                                            properties:
+                                              preference:
+                                                properties:
+                                                  matchExpressions:
+                                                    items:
+                                                      properties:
+                                                        key:
+                                                          type: string
+                                                        operator:
+                                                          type: string
+                                                        values:
+                                                          items:
+                                                            type: string
+                                                          type: array
+                                                      type: object
+                                                    type: array
+                                                  matchFields:
+                                                    items:
+                                                      properties:
+                                                        key:
+                                                          type: string
+                                                        operator:
+                                                          type: string
+                                                        values:
+                                                          items:
+                                                            type: string
+                                                          type: array
+                                                      type: object
+                                                    type: array
+                                                type: object
+                                              weight:
+                                                type: integer
+                                            type: object
+                                          type: array
+                                        requiredDuringSchedulingIgnoredDuringExecution:
+                                          properties:
+                                            nodeSelectorTerms:
+                                              items:
+                                                properties:
+                                                  matchExpressions:
+                                                    items:
+                                                      properties:
+                                                        key:
+                                                          type: string
+                                                        operator:
+                                                          type: string
+                                                        values:
+                                                          items:
+                                                            type: string
+                                                          type: array
+                                                      type: object
+                                                    type: array
+                                                  matchFields:
+                                                    items:
+                                                      properties:
+                                                        key:
+                                                          type: string
+                                                        operator:
+                                                          type: string
+                                                        values:
+                                                          items:
+                                                            type: string
+                                                          type: array
+                                                      type: object
+                                                    type: array
+                                                type: object
+                                              type: array
+                                          type: object
+                                      type: object
+                                    podAffinity:
+                                      properties:
+                                        preferredDuringSchedulingIgnoredDuringExecution:
+                                          items:
+                                            properties:
+                                              podAffinityTerm:
+                                                properties:
+                                                  labelSelector:
+                                                    properties:
+                                                      matchExpressions:
+                                                        items:
+                                                          properties:
+                                                            key:
+                                                              type: string
+                                                            operator:
+                                                              type: string
+                                                            values:
+                                                              items:
+                                                                type: string
+                                                              type: array
+                                                          type: object
+                                                        type: array
+                                                      matchLabels:
+                                                        additionalProperties:
+                                                          type: string
+                                                        type: object
+                                                    type: object
+                                                  matchLabelKeys:
+                                                    items:
+                                                      type: string
+                                                    type: array
+                                                  mismatchLabelKeys:
+                                                    items:
+                                                      type: string
+                                                    type: array
+                                                  namespaceSelector:
+                                                    properties:
+                                                      matchExpressions:
+                                                        items:
+                                                          properties:
+                                                            key:
+                                                              type: string
+                                                            operator:
+                                                              type: string
+                                                            values:
+                                                              items:
+                                                                type: string
+                                                              type: array
+                                                          type: object
+                                                        type: array
+                                                      matchLabels:
+                                                        additionalProperties:
+                                                          type: string
+                                                        type: object
+                                                    type: object
+                                                  namespaces:
+                                                    items:
+                                                      type: string
+                                                    type: array
+                                                  topologyKey:
+                                                    type: string
+                                                type: object
+                                              weight:
+                                                type: integer
+                                            type: object
+                                          type: array
+                                        requiredDuringSchedulingIgnoredDuringExecution:
+                                          items:
+                                            properties:
+                                              labelSelector:
+                                                properties:
+                                                  matchExpressions:
+                                                    items:
+                                                      properties:
+                                                        key:
+                                                          type: string
+                                                        operator:
+                                                          type: string
+                                                        values:
+                                                          items:
+                                                            type: string
+                                                          type: array
+                                                      type: object
+                                                    type: array
+                                                  matchLabels:
+                                                    additionalProperties:
+                                                      type: string
+                                                    type: object
+                                                type: object
+                                              matchLabelKeys:
+                                                items:
+                                                  type: string
+                                                type: array
+                                              mismatchLabelKeys:
+                                                items:
+                                                  type: string
+                                                type: array
+                                              namespaceSelector:
+                                                properties:
+                                                  matchExpressions:
+                                                    items:
+                                                      properties:
+                                                        key:
+                                                          type: string
+                                                        operator:
+                                                          type: string
+                                                        values:
+                                                          items:
+                                                            type: string
+                                                          type: array
+                                                      type: object
+                                                    type: array
+                                                  matchLabels:
+                                                    additionalProperties:
+                                                      type: string
+                                                    type: object
+                                                type: object
+                                              namespaces:
+                                                items:
+                                                  type: string
+                                                type: array
+                                              topologyKey:
+                                                type: string
+                                            type: object
+                                          type: array
+                                      type: object
+                                    podAntiAffinity:
+                                      properties:
+                                        preferredDuringSchedulingIgnoredDuringExecution:
+                                          items:
+                                            properties:
+                                              podAffinityTerm:
+                                                properties:
+                                                  labelSelector:
+                                                    properties:
+                                                      matchExpressions:
+                                                        items:
+                                                          properties:
+                                                            key:
+                                                              type: string
+                                                            operator:
+                                                              type: string
+                                                            values:
+                                                              items:
+                                                                type: string
+                                                              type: array
+                                                          type: object
+                                                        type: array
+                                                      matchLabels:
+                                                        additionalProperties:
+                                                          type: string
+                                                        type: object
+                                                    type: object
+                                                  matchLabelKeys:
+                                                    items:
+                                                      type: string
+                                                    type: array
+                                                  mismatchLabelKeys:
+                                                    items:
+                                                      type: string
+                                                    type: array
+                                                  namespaceSelector:
+                                                    properties:
+                                                      matchExpressions:
+                                                        items:
+                                                          properties:
+                                                            key:
+                                                              type: string
+                                                            operator:
+                                                              type: string
+                                                            values:
+                                                              items:
+                                                                type: string
+                                                              type: array
+                                                          type: object
+                                                        type: array
+                                                      matchLabels:
+                                                        additionalProperties:
+                                                          type: string
+                                                        type: object
+                                                    type: object
+                                                  namespaces:
+                                                    items:
+                                                      type: string
+                                                    type: array
+                                                  topologyKey:
+                                                    type: string
+                                                type: object
+                                              weight:
+                                                type: integer
+                                            type: object
+                                          type: array
+                                        requiredDuringSchedulingIgnoredDuringExecution:
+                                          items:
+                                            properties:
+                                              labelSelector:
+                                                properties:
+                                                  matchExpressions:
+                                                    items:
+                                                      properties:
+                                                        key:
+                                                          type: string
+                                                        operator:
+                                                          type: string
+                                                        values:
+                                                          items:
+                                                            type: string
+                                                          type: array
+                                                      type: object
+                                                    type: array
+                                                  matchLabels:
+                                                    additionalProperties:
+                                                      type: string
+                                                    type: object
+                                                type: object
+                                              matchLabelKeys:
+                                                items:
+                                                  type: string
+                                                type: array
+                                              mismatchLabelKeys:
+                                                items:
+                                                  type: string
+                                                type: array
+                                              namespaceSelector:
+                                                properties:
+                                                  matchExpressions:
+                                                    items:
+                                                      properties:
+                                                        key:
+                                                          type: string
+                                                        operator:
+                                                          type: string
+                                                        values:
+                                                          items:
+                                                            type: string
+                                                          type: array
+                                                      type: object
+                                                    type: array
+                                                  matchLabels:
+                                                    additionalProperties:
+                                                      type: string
+                                                    type: object
+                                                type: object
+                                              namespaces:
+                                                items:
+                                                  type: string
+                                                type: array
+                                              topologyKey:
+                                                type: string
+                                            type: object
+                                          type: array
+                                      type: object
+                                  type: object
+                                args:
+                                  items:
+                                    type: string
+                                  type: array
+                                exclude:
+                                  items:
+                                    type: string
+                                  type: array
+                                gitOptions:
+                                  type: string
+                                include:
+                                  items:
+                                    type: string
+                                  type: array
+                                name:
+                                  type: string
+                                resources:
+                                  properties:
+                                    claims:
+                                      items:
+                                        properties:
+                                          name:
+                                            type: string
+                                        type: object
+                                      type: array
+                                    limits:
+                                      additionalProperties:
+                                        anyOf:
+                                        - type: integer
+                                        - type: string
+                                        x-kubernetes-int-or-string: true
+                                      type: object
+                                    requests:
+                                      additionalProperties:
+                                        anyOf:
+                                        - type: integer
+                                        - type: string
+                                        x-kubernetes-int-or-string: true
+                                      type: object
+                                  type: object
+                                schedule:
+                                  type: string
+                                tolerations:
+                                  items:
+                                    properties:
+                                      effect:
+                                        type: string
+                                      key:
+                                        type: string
+                                      operator:
+                                        type: string
+                                      tolerationSeconds:
+                                        type: integer
+                                      value:
+                                        type: string
+                                    type: object
+                                  type: array
+                              type: object
+                            type: array
+                        type: object
+                    type: object
                   incomingReplication:
                     items:
                       properties:
diff --git a/crd/current/gerritindexers.gerritoperator.google.com-v1.yml b/crd/current/gerritindexers.gerritoperator.google.com-v1.yml
index c9abf64..5ee9983 100644
--- a/crd/current/gerritindexers.gerritoperator.google.com-v1.yml
+++ b/crd/current/gerritindexers.gerritoperator.google.com-v1.yml
@@ -13,7 +13,7 @@
     singular: gerritindexer
   scope: Namespaced
   versions:
-  - name: v1beta13
+  - name: v1beta14
     schema:
       openAPIV3Schema:
         properties:
diff --git a/crd/current/gerritmaintenances.gerritoperator.google.com-v1.yml b/crd/current/gerritmaintenances.gerritoperator.google.com-v1.yml
new file mode 100644
index 0000000..a1c9aba
--- /dev/null
+++ b/crd/current/gerritmaintenances.gerritoperator.google.com-v1.yml
@@ -0,0 +1,557 @@
+# Generated by Fabric8 CRDGenerator, manual edits might get overwritten!
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  name: gerritmaintenances.gerritoperator.google.com
+spec:
+  group: gerritoperator.google.com
+  names:
+    kind: GerritMaintenance
+    plural: gerritmaintenances
+    singular: gerritmaintenance
+  scope: Namespaced
+  versions:
+  - name: v1beta14
+    schema:
+      openAPIV3Schema:
+        properties:
+          spec:
+            properties:
+              containerImages:
+                properties:
+                  busyBox:
+                    properties:
+                      registry:
+                        type: string
+                      tag:
+                        type: string
+                    type: object
+                  gerritImages:
+                    properties:
+                      org:
+                        type: string
+                      registry:
+                        type: string
+                      tag:
+                        type: string
+                    type: object
+                  imagePullPolicy:
+                    type: string
+                  imagePullSecrets:
+                    items:
+                      properties:
+                        name:
+                          type: string
+                      type: object
+                    type: array
+                type: object
+              projects:
+                properties:
+                  gc:
+                    items:
+                      properties:
+                        affinity:
+                          properties:
+                            nodeAffinity:
+                              properties:
+                                preferredDuringSchedulingIgnoredDuringExecution:
+                                  items:
+                                    properties:
+                                      preference:
+                                        properties:
+                                          matchExpressions:
+                                            items:
+                                              properties:
+                                                key:
+                                                  type: string
+                                                operator:
+                                                  type: string
+                                                values:
+                                                  items:
+                                                    type: string
+                                                  type: array
+                                              type: object
+                                            type: array
+                                          matchFields:
+                                            items:
+                                              properties:
+                                                key:
+                                                  type: string
+                                                operator:
+                                                  type: string
+                                                values:
+                                                  items:
+                                                    type: string
+                                                  type: array
+                                              type: object
+                                            type: array
+                                        type: object
+                                      weight:
+                                        type: integer
+                                    type: object
+                                  type: array
+                                requiredDuringSchedulingIgnoredDuringExecution:
+                                  properties:
+                                    nodeSelectorTerms:
+                                      items:
+                                        properties:
+                                          matchExpressions:
+                                            items:
+                                              properties:
+                                                key:
+                                                  type: string
+                                                operator:
+                                                  type: string
+                                                values:
+                                                  items:
+                                                    type: string
+                                                  type: array
+                                              type: object
+                                            type: array
+                                          matchFields:
+                                            items:
+                                              properties:
+                                                key:
+                                                  type: string
+                                                operator:
+                                                  type: string
+                                                values:
+                                                  items:
+                                                    type: string
+                                                  type: array
+                                              type: object
+                                            type: array
+                                        type: object
+                                      type: array
+                                  type: object
+                              type: object
+                            podAffinity:
+                              properties:
+                                preferredDuringSchedulingIgnoredDuringExecution:
+                                  items:
+                                    properties:
+                                      podAffinityTerm:
+                                        properties:
+                                          labelSelector:
+                                            properties:
+                                              matchExpressions:
+                                                items:
+                                                  properties:
+                                                    key:
+                                                      type: string
+                                                    operator:
+                                                      type: string
+                                                    values:
+                                                      items:
+                                                        type: string
+                                                      type: array
+                                                  type: object
+                                                type: array
+                                              matchLabels:
+                                                additionalProperties:
+                                                  type: string
+                                                type: object
+                                            type: object
+                                          matchLabelKeys:
+                                            items:
+                                              type: string
+                                            type: array
+                                          mismatchLabelKeys:
+                                            items:
+                                              type: string
+                                            type: array
+                                          namespaceSelector:
+                                            properties:
+                                              matchExpressions:
+                                                items:
+                                                  properties:
+                                                    key:
+                                                      type: string
+                                                    operator:
+                                                      type: string
+                                                    values:
+                                                      items:
+                                                        type: string
+                                                      type: array
+                                                  type: object
+                                                type: array
+                                              matchLabels:
+                                                additionalProperties:
+                                                  type: string
+                                                type: object
+                                            type: object
+                                          namespaces:
+                                            items:
+                                              type: string
+                                            type: array
+                                          topologyKey:
+                                            type: string
+                                        type: object
+                                      weight:
+                                        type: integer
+                                    type: object
+                                  type: array
+                                requiredDuringSchedulingIgnoredDuringExecution:
+                                  items:
+                                    properties:
+                                      labelSelector:
+                                        properties:
+                                          matchExpressions:
+                                            items:
+                                              properties:
+                                                key:
+                                                  type: string
+                                                operator:
+                                                  type: string
+                                                values:
+                                                  items:
+                                                    type: string
+                                                  type: array
+                                              type: object
+                                            type: array
+                                          matchLabels:
+                                            additionalProperties:
+                                              type: string
+                                            type: object
+                                        type: object
+                                      matchLabelKeys:
+                                        items:
+                                          type: string
+                                        type: array
+                                      mismatchLabelKeys:
+                                        items:
+                                          type: string
+                                        type: array
+                                      namespaceSelector:
+                                        properties:
+                                          matchExpressions:
+                                            items:
+                                              properties:
+                                                key:
+                                                  type: string
+                                                operator:
+                                                  type: string
+                                                values:
+                                                  items:
+                                                    type: string
+                                                  type: array
+                                              type: object
+                                            type: array
+                                          matchLabels:
+                                            additionalProperties:
+                                              type: string
+                                            type: object
+                                        type: object
+                                      namespaces:
+                                        items:
+                                          type: string
+                                        type: array
+                                      topologyKey:
+                                        type: string
+                                    type: object
+                                  type: array
+                              type: object
+                            podAntiAffinity:
+                              properties:
+                                preferredDuringSchedulingIgnoredDuringExecution:
+                                  items:
+                                    properties:
+                                      podAffinityTerm:
+                                        properties:
+                                          labelSelector:
+                                            properties:
+                                              matchExpressions:
+                                                items:
+                                                  properties:
+                                                    key:
+                                                      type: string
+                                                    operator:
+                                                      type: string
+                                                    values:
+                                                      items:
+                                                        type: string
+                                                      type: array
+                                                  type: object
+                                                type: array
+                                              matchLabels:
+                                                additionalProperties:
+                                                  type: string
+                                                type: object
+                                            type: object
+                                          matchLabelKeys:
+                                            items:
+                                              type: string
+                                            type: array
+                                          mismatchLabelKeys:
+                                            items:
+                                              type: string
+                                            type: array
+                                          namespaceSelector:
+                                            properties:
+                                              matchExpressions:
+                                                items:
+                                                  properties:
+                                                    key:
+                                                      type: string
+                                                    operator:
+                                                      type: string
+                                                    values:
+                                                      items:
+                                                        type: string
+                                                      type: array
+                                                  type: object
+                                                type: array
+                                              matchLabels:
+                                                additionalProperties:
+                                                  type: string
+                                                type: object
+                                            type: object
+                                          namespaces:
+                                            items:
+                                              type: string
+                                            type: array
+                                          topologyKey:
+                                            type: string
+                                        type: object
+                                      weight:
+                                        type: integer
+                                    type: object
+                                  type: array
+                                requiredDuringSchedulingIgnoredDuringExecution:
+                                  items:
+                                    properties:
+                                      labelSelector:
+                                        properties:
+                                          matchExpressions:
+                                            items:
+                                              properties:
+                                                key:
+                                                  type: string
+                                                operator:
+                                                  type: string
+                                                values:
+                                                  items:
+                                                    type: string
+                                                  type: array
+                                              type: object
+                                            type: array
+                                          matchLabels:
+                                            additionalProperties:
+                                              type: string
+                                            type: object
+                                        type: object
+                                      matchLabelKeys:
+                                        items:
+                                          type: string
+                                        type: array
+                                      mismatchLabelKeys:
+                                        items:
+                                          type: string
+                                        type: array
+                                      namespaceSelector:
+                                        properties:
+                                          matchExpressions:
+                                            items:
+                                              properties:
+                                                key:
+                                                  type: string
+                                                operator:
+                                                  type: string
+                                                values:
+                                                  items:
+                                                    type: string
+                                                  type: array
+                                              type: object
+                                            type: array
+                                          matchLabels:
+                                            additionalProperties:
+                                              type: string
+                                            type: object
+                                        type: object
+                                      namespaces:
+                                        items:
+                                          type: string
+                                        type: array
+                                      topologyKey:
+                                        type: string
+                                    type: object
+                                  type: array
+                              type: object
+                          type: object
+                        args:
+                          items:
+                            type: string
+                          type: array
+                        exclude:
+                          items:
+                            type: string
+                          type: array
+                        gitOptions:
+                          type: string
+                        include:
+                          items:
+                            type: string
+                          type: array
+                        name:
+                          type: string
+                        resources:
+                          properties:
+                            claims:
+                              items:
+                                properties:
+                                  name:
+                                    type: string
+                                type: object
+                              type: array
+                            limits:
+                              additionalProperties:
+                                anyOf:
+                                - type: integer
+                                - type: string
+                                x-kubernetes-int-or-string: true
+                              type: object
+                            requests:
+                              additionalProperties:
+                                anyOf:
+                                - type: integer
+                                - type: string
+                                x-kubernetes-int-or-string: true
+                              type: object
+                          type: object
+                        schedule:
+                          type: string
+                        tolerations:
+                          items:
+                            properties:
+                              effect:
+                                type: string
+                              key:
+                                type: string
+                              operator:
+                                type: string
+                              tolerationSeconds:
+                                type: integer
+                              value:
+                                type: string
+                            type: object
+                          type: array
+                      type: object
+                    type: array
+                type: object
+              storage:
+                properties:
+                  pluginCache:
+                    properties:
+                      enabled:
+                        type: boolean
+                    type: object
+                  sharedStorage:
+                    properties:
+                      externalPVC:
+                        properties:
+                          claimName:
+                            type: string
+                          enabled:
+                            type: boolean
+                        type: object
+                      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
+                      size:
+                        anyOf:
+                        - type: integer
+                        - type: string
+                        x-kubernetes-int-or-string: true
+                      volumeName:
+                        type: string
+                    type: object
+                  storageClasses:
+                    properties:
+                      nfsWorkaround:
+                        properties:
+                          chownOnStartup:
+                            type: boolean
+                          enabled:
+                            type: boolean
+                          idmapdConfig:
+                            type: string
+                        type: object
+                      readWriteMany:
+                        type: string
+                      readWriteOnce:
+                        type: string
+                    type: object
+                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/crd/current/gerritnetworks.gerritoperator.google.com-v1.yml b/crd/current/gerritnetworks.gerritoperator.google.com-v1.yml
index 996e82e..569f41d 100644
--- a/crd/current/gerritnetworks.gerritoperator.google.com-v1.yml
+++ b/crd/current/gerritnetworks.gerritoperator.google.com-v1.yml
@@ -13,7 +13,7 @@
     singular: gerritnetwork
   scope: Namespaced
   versions:
-  - name: v1beta13
+  - name: v1beta14
     schema:
       openAPIV3Schema:
         properties:
diff --git a/crd/current/gerrits.gerritoperator.google.com-v1.yml b/crd/current/gerrits.gerritoperator.google.com-v1.yml
index aeead15..ffa491e 100644
--- a/crd/current/gerrits.gerritoperator.google.com-v1.yml
+++ b/crd/current/gerrits.gerritoperator.google.com-v1.yml
@@ -13,7 +13,7 @@
     singular: gerrit
   scope: Namespaced
   versions:
-  - name: v1beta13
+  - name: v1beta14
     schema:
       openAPIV3Schema:
         properties:
diff --git a/crd/current/gitgcs.gerritoperator.google.com-v1.yml b/crd/current/gitgcs.gerritoperator.google.com-v1.yml
index 52e0f04..c9259ec 100644
--- a/crd/current/gitgcs.gerritoperator.google.com-v1.yml
+++ b/crd/current/gitgcs.gerritoperator.google.com-v1.yml
@@ -13,7 +13,7 @@
     singular: gitgarbagecollection
   scope: Namespaced
   versions:
-  - name: v1beta13
+  - name: v1beta14
     schema:
       openAPIV3Schema:
         properties:
diff --git a/crd/current/incomingreplicationtasks.gerritoperator.google.com-v1.yml b/crd/current/incomingreplicationtasks.gerritoperator.google.com-v1.yml
index b104618..01ab21b 100644
--- a/crd/current/incomingreplicationtasks.gerritoperator.google.com-v1.yml
+++ b/crd/current/incomingreplicationtasks.gerritoperator.google.com-v1.yml
@@ -11,7 +11,7 @@
     singular: incomingreplicationtask
   scope: Namespaced
   versions:
-  - name: v1beta13
+  - name: v1beta14
     schema:
       openAPIV3Schema:
         properties:
diff --git a/crd/current/receivers.gerritoperator.google.com-v1.yml b/crd/current/receivers.gerritoperator.google.com-v1.yml
index 30b4ee3..e4a0c88 100644
--- a/crd/current/receivers.gerritoperator.google.com-v1.yml
+++ b/crd/current/receivers.gerritoperator.google.com-v1.yml
@@ -13,7 +13,7 @@
     singular: receiver
   scope: Namespaced
   versions:
-  - name: v1beta13
+  - name: v1beta14
     schema:
       openAPIV3Schema:
         properties:
diff --git a/crd/deprecated/gerritclusters.gerritoperator.google.com-v1.yml b/crd/deprecated/gerritclusters.gerritoperator.google.com-v1.yml
index e7b5c8b..8bf029b 100644
--- a/crd/deprecated/gerritclusters.gerritoperator.google.com-v1.yml
+++ b/crd/deprecated/gerritclusters.gerritoperator.google.com-v1.yml
@@ -13,7 +13,7 @@
     singular: gerritcluster
   scope: Namespaced
   versions:
-    - name: v1beta12
+    - name: v1beta13
       schema:
         openAPIV3Schema:
           properties:
diff --git a/crd/deprecated/gerritindexers.gerritoperator.google.com-v1.yml b/crd/deprecated/gerritindexers.gerritoperator.google.com-v1.yml
index 481c9da..11d7145 100644
--- a/crd/deprecated/gerritindexers.gerritoperator.google.com-v1.yml
+++ b/crd/deprecated/gerritindexers.gerritoperator.google.com-v1.yml
@@ -13,7 +13,7 @@
     singular: gerritindexer
   scope: Namespaced
   versions:
-    - name: v1beta12
+    - name: v1beta13
       schema:
         openAPIV3Schema:
           properties:
@@ -353,6 +353,21 @@
                   additionalProperties:
                     type: string
                   type: object
+                index:
+                  properties:
+                    elasticsearch:
+                      properties:
+                        config:
+                          type: string
+                        server:
+                          type: string
+                      type: object
+                    type:
+                      enum:
+                        - ELASTICSEARCH
+                        - LUCENE
+                      type: string
+                  type: object
                 resources:
                   properties:
                     claims:
diff --git a/crd/deprecated/gerritnetworks.gerritoperator.google.com-v1.yml b/crd/deprecated/gerritnetworks.gerritoperator.google.com-v1.yml
index 863bd40..cab3f85 100644
--- a/crd/deprecated/gerritnetworks.gerritoperator.google.com-v1.yml
+++ b/crd/deprecated/gerritnetworks.gerritoperator.google.com-v1.yml
@@ -13,7 +13,7 @@
     singular: gerritnetwork
   scope: Namespaced
   versions:
-    - name: v1beta12
+    - name: v1beta13
       schema:
         openAPIV3Schema:
           properties:
diff --git a/crd/deprecated/gerrits.gerritoperator.google.com-v1.yml b/crd/deprecated/gerrits.gerritoperator.google.com-v1.yml
index 49e3eef..a636f6f 100644
--- a/crd/deprecated/gerrits.gerritoperator.google.com-v1.yml
+++ b/crd/deprecated/gerrits.gerritoperator.google.com-v1.yml
@@ -13,7 +13,7 @@
     singular: gerrit
   scope: Namespaced
   versions:
-    - name: v1beta12
+    - name: v1beta13
       schema:
         openAPIV3Schema:
           properties:
diff --git a/crd/deprecated/gitgcs.gerritoperator.google.com-v1.yml b/crd/deprecated/gitgcs.gerritoperator.google.com-v1.yml
index f9494ff..6b5d2a2 100644
--- a/crd/deprecated/gitgcs.gerritoperator.google.com-v1.yml
+++ b/crd/deprecated/gitgcs.gerritoperator.google.com-v1.yml
@@ -13,7 +13,7 @@
     singular: gitgarbagecollection
   scope: Namespaced
   versions:
-    - name: v1beta12
+    - name: v1beta13
       schema:
         openAPIV3Schema:
           properties:
diff --git a/crd/deprecated/incomingreplicationtasks.gerritoperator.google.com-v1.yml b/crd/deprecated/incomingreplicationtasks.gerritoperator.google.com-v1.yml
index 2e463a6..59e4d21 100644
--- a/crd/deprecated/incomingreplicationtasks.gerritoperator.google.com-v1.yml
+++ b/crd/deprecated/incomingreplicationtasks.gerritoperator.google.com-v1.yml
@@ -11,7 +11,7 @@
     singular: incomingreplicationtask
   scope: Namespaced
   versions:
-    - name: v1beta12
+    - name: v1beta13
       schema:
         openAPIV3Schema:
           properties:
diff --git a/crd/deprecated/receivers.gerritoperator.google.com-v1.yml b/crd/deprecated/receivers.gerritoperator.google.com-v1.yml
index a3162b4..6414de7 100644
--- a/crd/deprecated/receivers.gerritoperator.google.com-v1.yml
+++ b/crd/deprecated/receivers.gerritoperator.google.com-v1.yml
@@ -13,7 +13,7 @@
     singular: receiver
   scope: Namespaced
   versions:
-    - name: v1beta12
+    - name: v1beta13
       schema:
         openAPIV3Schema:
           properties:
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 ca3fc02..e8964ca 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: v1beta13
+    - name: v1beta14
       schema:
         openAPIV3Schema:
           properties:
@@ -1587,6 +1587,402 @@
                   type: object
                 scheduledTasks:
                   properties:
+                    gerritMaintenance:
+                      properties:
+                        projects:
+                          properties:
+                            gc:
+                              items:
+                                properties:
+                                  affinity:
+                                    properties:
+                                      nodeAffinity:
+                                        properties:
+                                          preferredDuringSchedulingIgnoredDuringExecution:
+                                            items:
+                                              properties:
+                                                preference:
+                                                  properties:
+                                                    matchExpressions:
+                                                      items:
+                                                        properties:
+                                                          key:
+                                                            type: string
+                                                          operator:
+                                                            type: string
+                                                          values:
+                                                            items:
+                                                              type: string
+                                                            type: array
+                                                        type: object
+                                                      type: array
+                                                    matchFields:
+                                                      items:
+                                                        properties:
+                                                          key:
+                                                            type: string
+                                                          operator:
+                                                            type: string
+                                                          values:
+                                                            items:
+                                                              type: string
+                                                            type: array
+                                                        type: object
+                                                      type: array
+                                                  type: object
+                                                weight:
+                                                  type: integer
+                                              type: object
+                                            type: array
+                                          requiredDuringSchedulingIgnoredDuringExecution:
+                                            properties:
+                                              nodeSelectorTerms:
+                                                items:
+                                                  properties:
+                                                    matchExpressions:
+                                                      items:
+                                                        properties:
+                                                          key:
+                                                            type: string
+                                                          operator:
+                                                            type: string
+                                                          values:
+                                                            items:
+                                                              type: string
+                                                            type: array
+                                                        type: object
+                                                      type: array
+                                                    matchFields:
+                                                      items:
+                                                        properties:
+                                                          key:
+                                                            type: string
+                                                          operator:
+                                                            type: string
+                                                          values:
+                                                            items:
+                                                              type: string
+                                                            type: array
+                                                        type: object
+                                                      type: array
+                                                  type: object
+                                                type: array
+                                            type: object
+                                        type: object
+                                      podAffinity:
+                                        properties:
+                                          preferredDuringSchedulingIgnoredDuringExecution:
+                                            items:
+                                              properties:
+                                                podAffinityTerm:
+                                                  properties:
+                                                    labelSelector:
+                                                      properties:
+                                                        matchExpressions:
+                                                          items:
+                                                            properties:
+                                                              key:
+                                                                type: string
+                                                              operator:
+                                                                type: string
+                                                              values:
+                                                                items:
+                                                                  type: string
+                                                                type: array
+                                                            type: object
+                                                          type: array
+                                                        matchLabels:
+                                                          additionalProperties:
+                                                            type: string
+                                                          type: object
+                                                      type: object
+                                                    matchLabelKeys:
+                                                      items:
+                                                        type: string
+                                                      type: array
+                                                    mismatchLabelKeys:
+                                                      items:
+                                                        type: string
+                                                      type: array
+                                                    namespaceSelector:
+                                                      properties:
+                                                        matchExpressions:
+                                                          items:
+                                                            properties:
+                                                              key:
+                                                                type: string
+                                                              operator:
+                                                                type: string
+                                                              values:
+                                                                items:
+                                                                  type: string
+                                                                type: array
+                                                            type: object
+                                                          type: array
+                                                        matchLabels:
+                                                          additionalProperties:
+                                                            type: string
+                                                          type: object
+                                                      type: object
+                                                    namespaces:
+                                                      items:
+                                                        type: string
+                                                      type: array
+                                                    topologyKey:
+                                                      type: string
+                                                  type: object
+                                                weight:
+                                                  type: integer
+                                              type: object
+                                            type: array
+                                          requiredDuringSchedulingIgnoredDuringExecution:
+                                            items:
+                                              properties:
+                                                labelSelector:
+                                                  properties:
+                                                    matchExpressions:
+                                                      items:
+                                                        properties:
+                                                          key:
+                                                            type: string
+                                                          operator:
+                                                            type: string
+                                                          values:
+                                                            items:
+                                                              type: string
+                                                            type: array
+                                                        type: object
+                                                      type: array
+                                                    matchLabels:
+                                                      additionalProperties:
+                                                        type: string
+                                                      type: object
+                                                  type: object
+                                                matchLabelKeys:
+                                                  items:
+                                                    type: string
+                                                  type: array
+                                                mismatchLabelKeys:
+                                                  items:
+                                                    type: string
+                                                  type: array
+                                                namespaceSelector:
+                                                  properties:
+                                                    matchExpressions:
+                                                      items:
+                                                        properties:
+                                                          key:
+                                                            type: string
+                                                          operator:
+                                                            type: string
+                                                          values:
+                                                            items:
+                                                              type: string
+                                                            type: array
+                                                        type: object
+                                                      type: array
+                                                    matchLabels:
+                                                      additionalProperties:
+                                                        type: string
+                                                      type: object
+                                                  type: object
+                                                namespaces:
+                                                  items:
+                                                    type: string
+                                                  type: array
+                                                topologyKey:
+                                                  type: string
+                                              type: object
+                                            type: array
+                                        type: object
+                                      podAntiAffinity:
+                                        properties:
+                                          preferredDuringSchedulingIgnoredDuringExecution:
+                                            items:
+                                              properties:
+                                                podAffinityTerm:
+                                                  properties:
+                                                    labelSelector:
+                                                      properties:
+                                                        matchExpressions:
+                                                          items:
+                                                            properties:
+                                                              key:
+                                                                type: string
+                                                              operator:
+                                                                type: string
+                                                              values:
+                                                                items:
+                                                                  type: string
+                                                                type: array
+                                                            type: object
+                                                          type: array
+                                                        matchLabels:
+                                                          additionalProperties:
+                                                            type: string
+                                                          type: object
+                                                      type: object
+                                                    matchLabelKeys:
+                                                      items:
+                                                        type: string
+                                                      type: array
+                                                    mismatchLabelKeys:
+                                                      items:
+                                                        type: string
+                                                      type: array
+                                                    namespaceSelector:
+                                                      properties:
+                                                        matchExpressions:
+                                                          items:
+                                                            properties:
+                                                              key:
+                                                                type: string
+                                                              operator:
+                                                                type: string
+                                                              values:
+                                                                items:
+                                                                  type: string
+                                                                type: array
+                                                            type: object
+                                                          type: array
+                                                        matchLabels:
+                                                          additionalProperties:
+                                                            type: string
+                                                          type: object
+                                                      type: object
+                                                    namespaces:
+                                                      items:
+                                                        type: string
+                                                      type: array
+                                                    topologyKey:
+                                                      type: string
+                                                  type: object
+                                                weight:
+                                                  type: integer
+                                              type: object
+                                            type: array
+                                          requiredDuringSchedulingIgnoredDuringExecution:
+                                            items:
+                                              properties:
+                                                labelSelector:
+                                                  properties:
+                                                    matchExpressions:
+                                                      items:
+                                                        properties:
+                                                          key:
+                                                            type: string
+                                                          operator:
+                                                            type: string
+                                                          values:
+                                                            items:
+                                                              type: string
+                                                            type: array
+                                                        type: object
+                                                      type: array
+                                                    matchLabels:
+                                                      additionalProperties:
+                                                        type: string
+                                                      type: object
+                                                  type: object
+                                                matchLabelKeys:
+                                                  items:
+                                                    type: string
+                                                  type: array
+                                                mismatchLabelKeys:
+                                                  items:
+                                                    type: string
+                                                  type: array
+                                                namespaceSelector:
+                                                  properties:
+                                                    matchExpressions:
+                                                      items:
+                                                        properties:
+                                                          key:
+                                                            type: string
+                                                          operator:
+                                                            type: string
+                                                          values:
+                                                            items:
+                                                              type: string
+                                                            type: array
+                                                        type: object
+                                                      type: array
+                                                    matchLabels:
+                                                      additionalProperties:
+                                                        type: string
+                                                      type: object
+                                                  type: object
+                                                namespaces:
+                                                  items:
+                                                    type: string
+                                                  type: array
+                                                topologyKey:
+                                                  type: string
+                                              type: object
+                                            type: array
+                                        type: object
+                                    type: object
+                                  args:
+                                    items:
+                                      type: string
+                                    type: array
+                                  exclude:
+                                    items:
+                                      type: string
+                                    type: array
+                                  gitOptions:
+                                    type: string
+                                  include:
+                                    items:
+                                      type: string
+                                    type: array
+                                  name:
+                                    type: string
+                                  resources:
+                                    properties:
+                                      claims:
+                                        items:
+                                          properties:
+                                            name:
+                                              type: string
+                                          type: object
+                                        type: array
+                                      limits:
+                                        additionalProperties:
+                                          anyOf:
+                                            - type: integer
+                                            - type: string
+                                          x-kubernetes-int-or-string: true
+                                        type: object
+                                      requests:
+                                        additionalProperties:
+                                          anyOf:
+                                            - type: integer
+                                            - type: string
+                                          x-kubernetes-int-or-string: true
+                                        type: object
+                                    type: object
+                                  schedule:
+                                    type: string
+                                  tolerations:
+                                    items:
+                                      properties:
+                                        effect:
+                                          type: string
+                                        key:
+                                          type: string
+                                        operator:
+                                          type: string
+                                        tolerationSeconds:
+                                          type: integer
+                                        value:
+                                          type: string
+                                      type: object
+                                    type: array
+                                type: object
+                              type: array
+                          type: object
+                      type: object
                     incomingReplication:
                       items:
                         properties:
@@ -2151,7 +2547,7 @@
       storage: true
       subresources:
         status: {}
-    - name: v1beta12
+    - name: v1beta13
       schema:
         openAPIV3Schema:
           properties:
diff --git a/helm-charts/gerrit-operator-crds/templates/gerritindexers.gerritoperator.google.com-v1.yml b/helm-charts/gerrit-operator-crds/templates/gerritindexers.gerritoperator.google.com-v1.yml
index d3ceba0..02de042 100644
--- a/helm-charts/gerrit-operator-crds/templates/gerritindexers.gerritoperator.google.com-v1.yml
+++ b/helm-charts/gerrit-operator-crds/templates/gerritindexers.gerritoperator.google.com-v1.yml
@@ -13,7 +13,7 @@
     singular: gerritindexer
   scope: Namespaced
   versions:
-    - name: v1beta13
+    - name: v1beta14
       schema:
         openAPIV3Schema:
           properties:
@@ -487,7 +487,7 @@
       storage: true
       subresources:
         status: {}
-    - name: v1beta12
+    - name: v1beta13
       schema:
         openAPIV3Schema:
           properties:
@@ -827,6 +827,21 @@
                   additionalProperties:
                     type: string
                   type: object
+                index:
+                  properties:
+                    elasticsearch:
+                      properties:
+                        config:
+                          type: string
+                        server:
+                          type: string
+                      type: object
+                    type:
+                      enum:
+                        - ELASTICSEARCH
+                        - LUCENE
+                      type: string
+                  type: object
                 resources:
                   properties:
                     claims:
diff --git a/helm-charts/gerrit-operator-crds/templates/gerritmaintenances.gerritoperator.google.com-v1.yml b/helm-charts/gerrit-operator-crds/templates/gerritmaintenances.gerritoperator.google.com-v1.yml
new file mode 100644
index 0000000..a1c9aba
--- /dev/null
+++ b/helm-charts/gerrit-operator-crds/templates/gerritmaintenances.gerritoperator.google.com-v1.yml
@@ -0,0 +1,557 @@
+# Generated by Fabric8 CRDGenerator, manual edits might get overwritten!
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  name: gerritmaintenances.gerritoperator.google.com
+spec:
+  group: gerritoperator.google.com
+  names:
+    kind: GerritMaintenance
+    plural: gerritmaintenances
+    singular: gerritmaintenance
+  scope: Namespaced
+  versions:
+  - name: v1beta14
+    schema:
+      openAPIV3Schema:
+        properties:
+          spec:
+            properties:
+              containerImages:
+                properties:
+                  busyBox:
+                    properties:
+                      registry:
+                        type: string
+                      tag:
+                        type: string
+                    type: object
+                  gerritImages:
+                    properties:
+                      org:
+                        type: string
+                      registry:
+                        type: string
+                      tag:
+                        type: string
+                    type: object
+                  imagePullPolicy:
+                    type: string
+                  imagePullSecrets:
+                    items:
+                      properties:
+                        name:
+                          type: string
+                      type: object
+                    type: array
+                type: object
+              projects:
+                properties:
+                  gc:
+                    items:
+                      properties:
+                        affinity:
+                          properties:
+                            nodeAffinity:
+                              properties:
+                                preferredDuringSchedulingIgnoredDuringExecution:
+                                  items:
+                                    properties:
+                                      preference:
+                                        properties:
+                                          matchExpressions:
+                                            items:
+                                              properties:
+                                                key:
+                                                  type: string
+                                                operator:
+                                                  type: string
+                                                values:
+                                                  items:
+                                                    type: string
+                                                  type: array
+                                              type: object
+                                            type: array
+                                          matchFields:
+                                            items:
+                                              properties:
+                                                key:
+                                                  type: string
+                                                operator:
+                                                  type: string
+                                                values:
+                                                  items:
+                                                    type: string
+                                                  type: array
+                                              type: object
+                                            type: array
+                                        type: object
+                                      weight:
+                                        type: integer
+                                    type: object
+                                  type: array
+                                requiredDuringSchedulingIgnoredDuringExecution:
+                                  properties:
+                                    nodeSelectorTerms:
+                                      items:
+                                        properties:
+                                          matchExpressions:
+                                            items:
+                                              properties:
+                                                key:
+                                                  type: string
+                                                operator:
+                                                  type: string
+                                                values:
+                                                  items:
+                                                    type: string
+                                                  type: array
+                                              type: object
+                                            type: array
+                                          matchFields:
+                                            items:
+                                              properties:
+                                                key:
+                                                  type: string
+                                                operator:
+                                                  type: string
+                                                values:
+                                                  items:
+                                                    type: string
+                                                  type: array
+                                              type: object
+                                            type: array
+                                        type: object
+                                      type: array
+                                  type: object
+                              type: object
+                            podAffinity:
+                              properties:
+                                preferredDuringSchedulingIgnoredDuringExecution:
+                                  items:
+                                    properties:
+                                      podAffinityTerm:
+                                        properties:
+                                          labelSelector:
+                                            properties:
+                                              matchExpressions:
+                                                items:
+                                                  properties:
+                                                    key:
+                                                      type: string
+                                                    operator:
+                                                      type: string
+                                                    values:
+                                                      items:
+                                                        type: string
+                                                      type: array
+                                                  type: object
+                                                type: array
+                                              matchLabels:
+                                                additionalProperties:
+                                                  type: string
+                                                type: object
+                                            type: object
+                                          matchLabelKeys:
+                                            items:
+                                              type: string
+                                            type: array
+                                          mismatchLabelKeys:
+                                            items:
+                                              type: string
+                                            type: array
+                                          namespaceSelector:
+                                            properties:
+                                              matchExpressions:
+                                                items:
+                                                  properties:
+                                                    key:
+                                                      type: string
+                                                    operator:
+                                                      type: string
+                                                    values:
+                                                      items:
+                                                        type: string
+                                                      type: array
+                                                  type: object
+                                                type: array
+                                              matchLabels:
+                                                additionalProperties:
+                                                  type: string
+                                                type: object
+                                            type: object
+                                          namespaces:
+                                            items:
+                                              type: string
+                                            type: array
+                                          topologyKey:
+                                            type: string
+                                        type: object
+                                      weight:
+                                        type: integer
+                                    type: object
+                                  type: array
+                                requiredDuringSchedulingIgnoredDuringExecution:
+                                  items:
+                                    properties:
+                                      labelSelector:
+                                        properties:
+                                          matchExpressions:
+                                            items:
+                                              properties:
+                                                key:
+                                                  type: string
+                                                operator:
+                                                  type: string
+                                                values:
+                                                  items:
+                                                    type: string
+                                                  type: array
+                                              type: object
+                                            type: array
+                                          matchLabels:
+                                            additionalProperties:
+                                              type: string
+                                            type: object
+                                        type: object
+                                      matchLabelKeys:
+                                        items:
+                                          type: string
+                                        type: array
+                                      mismatchLabelKeys:
+                                        items:
+                                          type: string
+                                        type: array
+                                      namespaceSelector:
+                                        properties:
+                                          matchExpressions:
+                                            items:
+                                              properties:
+                                                key:
+                                                  type: string
+                                                operator:
+                                                  type: string
+                                                values:
+                                                  items:
+                                                    type: string
+                                                  type: array
+                                              type: object
+                                            type: array
+                                          matchLabels:
+                                            additionalProperties:
+                                              type: string
+                                            type: object
+                                        type: object
+                                      namespaces:
+                                        items:
+                                          type: string
+                                        type: array
+                                      topologyKey:
+                                        type: string
+                                    type: object
+                                  type: array
+                              type: object
+                            podAntiAffinity:
+                              properties:
+                                preferredDuringSchedulingIgnoredDuringExecution:
+                                  items:
+                                    properties:
+                                      podAffinityTerm:
+                                        properties:
+                                          labelSelector:
+                                            properties:
+                                              matchExpressions:
+                                                items:
+                                                  properties:
+                                                    key:
+                                                      type: string
+                                                    operator:
+                                                      type: string
+                                                    values:
+                                                      items:
+                                                        type: string
+                                                      type: array
+                                                  type: object
+                                                type: array
+                                              matchLabels:
+                                                additionalProperties:
+                                                  type: string
+                                                type: object
+                                            type: object
+                                          matchLabelKeys:
+                                            items:
+                                              type: string
+                                            type: array
+                                          mismatchLabelKeys:
+                                            items:
+                                              type: string
+                                            type: array
+                                          namespaceSelector:
+                                            properties:
+                                              matchExpressions:
+                                                items:
+                                                  properties:
+                                                    key:
+                                                      type: string
+                                                    operator:
+                                                      type: string
+                                                    values:
+                                                      items:
+                                                        type: string
+                                                      type: array
+                                                  type: object
+                                                type: array
+                                              matchLabels:
+                                                additionalProperties:
+                                                  type: string
+                                                type: object
+                                            type: object
+                                          namespaces:
+                                            items:
+                                              type: string
+                                            type: array
+                                          topologyKey:
+                                            type: string
+                                        type: object
+                                      weight:
+                                        type: integer
+                                    type: object
+                                  type: array
+                                requiredDuringSchedulingIgnoredDuringExecution:
+                                  items:
+                                    properties:
+                                      labelSelector:
+                                        properties:
+                                          matchExpressions:
+                                            items:
+                                              properties:
+                                                key:
+                                                  type: string
+                                                operator:
+                                                  type: string
+                                                values:
+                                                  items:
+                                                    type: string
+                                                  type: array
+                                              type: object
+                                            type: array
+                                          matchLabels:
+                                            additionalProperties:
+                                              type: string
+                                            type: object
+                                        type: object
+                                      matchLabelKeys:
+                                        items:
+                                          type: string
+                                        type: array
+                                      mismatchLabelKeys:
+                                        items:
+                                          type: string
+                                        type: array
+                                      namespaceSelector:
+                                        properties:
+                                          matchExpressions:
+                                            items:
+                                              properties:
+                                                key:
+                                                  type: string
+                                                operator:
+                                                  type: string
+                                                values:
+                                                  items:
+                                                    type: string
+                                                  type: array
+                                              type: object
+                                            type: array
+                                          matchLabels:
+                                            additionalProperties:
+                                              type: string
+                                            type: object
+                                        type: object
+                                      namespaces:
+                                        items:
+                                          type: string
+                                        type: array
+                                      topologyKey:
+                                        type: string
+                                    type: object
+                                  type: array
+                              type: object
+                          type: object
+                        args:
+                          items:
+                            type: string
+                          type: array
+                        exclude:
+                          items:
+                            type: string
+                          type: array
+                        gitOptions:
+                          type: string
+                        include:
+                          items:
+                            type: string
+                          type: array
+                        name:
+                          type: string
+                        resources:
+                          properties:
+                            claims:
+                              items:
+                                properties:
+                                  name:
+                                    type: string
+                                type: object
+                              type: array
+                            limits:
+                              additionalProperties:
+                                anyOf:
+                                - type: integer
+                                - type: string
+                                x-kubernetes-int-or-string: true
+                              type: object
+                            requests:
+                              additionalProperties:
+                                anyOf:
+                                - type: integer
+                                - type: string
+                                x-kubernetes-int-or-string: true
+                              type: object
+                          type: object
+                        schedule:
+                          type: string
+                        tolerations:
+                          items:
+                            properties:
+                              effect:
+                                type: string
+                              key:
+                                type: string
+                              operator:
+                                type: string
+                              tolerationSeconds:
+                                type: integer
+                              value:
+                                type: string
+                            type: object
+                          type: array
+                      type: object
+                    type: array
+                type: object
+              storage:
+                properties:
+                  pluginCache:
+                    properties:
+                      enabled:
+                        type: boolean
+                    type: object
+                  sharedStorage:
+                    properties:
+                      externalPVC:
+                        properties:
+                          claimName:
+                            type: string
+                          enabled:
+                            type: boolean
+                        type: object
+                      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
+                      size:
+                        anyOf:
+                        - type: integer
+                        - type: string
+                        x-kubernetes-int-or-string: true
+                      volumeName:
+                        type: string
+                    type: object
+                  storageClasses:
+                    properties:
+                      nfsWorkaround:
+                        properties:
+                          chownOnStartup:
+                            type: boolean
+                          enabled:
+                            type: boolean
+                          idmapdConfig:
+                            type: string
+                        type: object
+                      readWriteMany:
+                        type: string
+                      readWriteOnce:
+                        type: string
+                    type: object
+                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/gerritnetworks.gerritoperator.google.com-v1.yml b/helm-charts/gerrit-operator-crds/templates/gerritnetworks.gerritoperator.google.com-v1.yml
index 8e2cbcb..dd115aa 100644
--- 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
@@ -13,7 +13,7 @@
     singular: gerritnetwork
   scope: Namespaced
   versions:
-    - name: v1beta13
+    - name: v1beta14
       schema:
         openAPIV3Schema:
           properties:
@@ -139,7 +139,7 @@
       storage: true
       subresources:
         status: {}
-    - name: v1beta12
+    - name: v1beta13
       schema:
         openAPIV3Schema:
           properties:
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 2fcf92d..6da220c 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: v1beta13
+    - name: v1beta14
       schema:
         openAPIV3Schema:
           properties:
@@ -933,7 +933,7 @@
       storage: true
       subresources:
         status: {}
-    - name: v1beta12
+    - name: v1beta13
       schema:
         openAPIV3Schema:
           properties:
diff --git a/helm-charts/gerrit-operator-crds/templates/gitgcs.gerritoperator.google.com-v1.yml b/helm-charts/gerrit-operator-crds/templates/gitgcs.gerritoperator.google.com-v1.yml
index cd97d09..3c921f3 100644
--- a/helm-charts/gerrit-operator-crds/templates/gitgcs.gerritoperator.google.com-v1.yml
+++ b/helm-charts/gerrit-operator-crds/templates/gitgcs.gerritoperator.google.com-v1.yml
@@ -13,7 +13,7 @@
     singular: gitgarbagecollection
   scope: Namespaced
   versions:
-    - name: v1beta13
+    - name: v1beta14
       schema:
         openAPIV3Schema:
           properties:
@@ -422,7 +422,7 @@
       storage: true
       subresources:
         status: {}
-    - name: v1beta12
+    - name: v1beta13
       schema:
         openAPIV3Schema:
           properties:
diff --git a/helm-charts/gerrit-operator-crds/templates/incomingreplicationtasks.gerritoperator.google.com-v1.yml b/helm-charts/gerrit-operator-crds/templates/incomingreplicationtasks.gerritoperator.google.com-v1.yml
index 6d89cbf..84a91b8 100644
--- a/helm-charts/gerrit-operator-crds/templates/incomingreplicationtasks.gerritoperator.google.com-v1.yml
+++ b/helm-charts/gerrit-operator-crds/templates/incomingreplicationtasks.gerritoperator.google.com-v1.yml
@@ -11,7 +11,7 @@
     singular: incomingreplicationtask
   scope: Namespaced
   versions:
-    - name: v1beta13
+    - name: v1beta14
       schema:
         openAPIV3Schema:
           properties:
@@ -568,7 +568,7 @@
       storage: true
       subresources:
         status: {}
-    - name: v1beta12
+    - name: v1beta13
       schema:
         openAPIV3Schema:
           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 7bf3694..005f62f 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: v1beta13
+    - name: v1beta14
       schema:
         openAPIV3Schema:
           properties:
@@ -685,7 +685,7 @@
       storage: true
       subresources:
         status: {}
-    - name: v1beta12
+    - name: v1beta13
       schema:
         openAPIV3Schema:
           properties:
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/Constants.java b/operator/src/main/java/com/google/gerrit/k8s/operator/Constants.java
index 9e30002..3715a75 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/Constants.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/Constants.java
@@ -17,15 +17,16 @@
 import com.google.inject.AbstractModule;
 
 public class Constants extends AbstractModule {
-  public static final String VERSION = "v1beta13";
+  public static final String VERSION = "v1beta14";
 
   // The resource kind always has to be plural for use in webhooks
   public static final String GERRIT_CLUSTER_KIND = "gerritclusters";
   public static final String GERRIT_KIND = "gerrits";
   public static final String GIT_GC_KIND = "gitgarbagecollections";
+  public static final String GERRIT_MAINTENANCE_KIND = "gerritmaintenances";
 
   public static final String[] RESOURCES_WITH_VALIDATING_WEBHOOK =
-      new String[] {GERRIT_CLUSTER_KIND, GERRIT_KIND, GIT_GC_KIND};
+      new String[] {GERRIT_CLUSTER_KIND, GERRIT_KIND, GIT_GC_KIND, GERRIT_MAINTENANCE_KIND};
 
   public static final long GERRIT_USER_ID = 1000L;
   public static final long GERRIT_USER_GROUP_ID = 100L;
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 c22384b..a81f1ad 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
@@ -21,6 +21,7 @@
 import com.google.gerrit.k8s.operator.gerrit.GerritReconciler;
 import com.google.gerrit.k8s.operator.gitgc.GitGarbageCollectionReconciler;
 import com.google.gerrit.k8s.operator.indexer.GerritIndexerReconciler;
+import com.google.gerrit.k8s.operator.maintenance.GerritMaintenanceReconciler;
 import com.google.gerrit.k8s.operator.network.GerritNetworkReconcilerProvider;
 import com.google.gerrit.k8s.operator.network.istio.GerritIstioReconciler;
 import com.google.gerrit.k8s.operator.receiver.ReceiverReconciler;
@@ -65,6 +66,7 @@
     } else {
       reconcilers.addBinding().to(GerritClusterReconciler.class);
       reconcilers.addBinding().to(GitGarbageCollectionReconciler.class);
+      reconcilers.addBinding().to(GerritMaintenanceReconciler.class);
       reconcilers.addBinding().to(IncomingReplicationTaskReconciler.class);
       reconcilers.addBinding().to(GerritIndexerReconciler.class);
       reconcilers.addBinding().to(ReceiverReconciler.class);
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/admission/servlet/GerritClusterAdmissionWebhook.java b/operator/src/main/java/com/google/gerrit/k8s/operator/admission/servlet/GerritClusterAdmissionWebhook.java
index eabbd11..df5a7cb 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/admission/servlet/GerritClusterAdmissionWebhook.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/admission/servlet/GerritClusterAdmissionWebhook.java
@@ -19,6 +19,9 @@
 import com.google.gerrit.k8s.operator.api.model.cluster.GerritCluster;
 import com.google.gerrit.k8s.operator.api.model.gerrit.GerritTemplate;
 import com.google.gerrit.k8s.operator.api.model.gerrit.GerritTemplateSpec.GerritMode;
+import com.google.gerrit.k8s.operator.api.model.maintenance.GerritMaintenance;
+import com.google.gerrit.k8s.operator.api.model.maintenance.GerritMaintenanceSpec;
+import com.google.gerrit.k8s.operator.api.model.maintenance.GerritMaintenanceSpecTemplate;
 import com.google.gerrit.k8s.operator.server.ValidatingAdmissionWebhookServlet;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -106,6 +109,20 @@
       }
     }
 
+    GerritMaintenanceSpecTemplate gmSpecTemplate =
+        gerritCluster.getSpec().getScheduledTasks().getGerritMaintenance();
+    if (gmSpecTemplate != null) {
+      GerritMaintenanceAdmissionWebhook gerritMaintenanceAdmission =
+          new GerritMaintenanceAdmissionWebhook();
+      GerritMaintenanceSpec spec = gmSpecTemplate.toGerritMaintenanceSpec(gerritCluster);
+      GerritMaintenance maintenance = new GerritMaintenance();
+      maintenance.setSpec(spec);
+      Status status = gerritMaintenanceAdmission.validate(maintenance);
+      if (status.getCode() != HttpServletResponse.SC_OK) {
+        return status;
+      }
+    }
+
     return new StatusBuilder().withCode(HttpServletResponse.SC_OK).build();
   }
 
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/admission/servlet/GerritMaintenanceAdmissionWebhook.java b/operator/src/main/java/com/google/gerrit/k8s/operator/admission/servlet/GerritMaintenanceAdmissionWebhook.java
new file mode 100644
index 0000000..0e267b3
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/admission/servlet/GerritMaintenanceAdmissionWebhook.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.admission.servlet;
+
+import com.google.gerrit.k8s.operator.Constants;
+import com.google.gerrit.k8s.operator.api.model.maintenance.GerritMaintenance;
+import com.google.gerrit.k8s.operator.api.model.maintenance.GitGcTask;
+import com.google.gerrit.k8s.operator.maintenance.dependent.GerritMaintenanceCronJobs;
+import com.google.gerrit.k8s.operator.maintenance.dependent.GerritMaintenanceTaskConflictException;
+import com.google.gerrit.k8s.operator.server.ValidatingAdmissionWebhookServlet;
+import com.google.inject.Singleton;
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.fabric8.kubernetes.api.model.Status;
+import io.fabric8.kubernetes.api.model.StatusBuilder;
+import jakarta.servlet.http.HttpServletResponse;
+import java.util.List;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+
+@Singleton
+public class GerritMaintenanceAdmissionWebhook extends ValidatingAdmissionWebhookServlet {
+  private static final long serialVersionUID = 1L;
+
+  @Override
+  public Status validate(HasMetadata resource) {
+    if (!(resource instanceof GerritMaintenance)) {
+      return new StatusBuilder()
+          .withCode(HttpServletResponse.SC_BAD_REQUEST)
+          .withMessage("Invalid resource. Expected GerritMaintenance-resource for validation.")
+          .build();
+    }
+
+    GerritMaintenance gm = (GerritMaintenance) resource;
+    List<GitGcTask> gcTasks = gm.getSpec().getProjects().getGc();
+    try {
+      GerritMaintenanceCronJobs.checkForConflict(gcTasks);
+    } catch (GerritMaintenanceTaskConflictException e) {
+      return new StatusBuilder()
+          .withCode(HttpServletResponse.SC_CONFLICT)
+          .withMessage(e.getMessage())
+          .build();
+    }
+
+    try {
+      Config cfg = new Config();
+      for (GitGcTask gcTask : gcTasks) {
+        String gitOptions = gcTask.getGitOptions();
+        if (gitOptions != null) {
+          cfg.fromText(gcTask.getGitOptions());
+        }
+      }
+    } catch (ConfigInvalidException e) {
+      return new StatusBuilder()
+          .withCode(HttpServletResponse.SC_BAD_REQUEST)
+          .withMessage(e.getMessage())
+          .build();
+    }
+
+    return new StatusBuilder().withCode(HttpServletResponse.SC_OK).build();
+  }
+
+  @Override
+  public String getName() {
+    return Constants.GERRIT_MAINTENANCE_KIND;
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/admission/servlet/GitGcAdmissionWebhook.java b/operator/src/main/java/com/google/gerrit/k8s/operator/admission/servlet/GitGcAdmissionWebhook.java
index e23a5c2..156c470 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/admission/servlet/GitGcAdmissionWebhook.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/admission/servlet/GitGcAdmissionWebhook.java
@@ -32,6 +32,7 @@
 import java.util.stream.Collectors;
 
 @Singleton
+@Deprecated
 public class GitGcAdmissionWebhook extends ValidatingAdmissionWebhookServlet {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
   private static final long serialVersionUID = 1L;
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/cluster/ScheduledTasks.java b/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/cluster/ScheduledTasks.java
index 3651d32..1155ec4 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/cluster/ScheduledTasks.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/cluster/ScheduledTasks.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.k8s.operator.api.model.cluster;
 
+import com.google.gerrit.k8s.operator.api.model.maintenance.GerritMaintenanceSpecTemplate;
 import com.google.gerrit.k8s.operator.api.model.tasks.incomingrepl.IncomingReplicationTaskTemplate;
 import java.util.ArrayList;
 import java.util.List;
@@ -21,6 +22,7 @@
 
 public class ScheduledTasks {
   private List<IncomingReplicationTaskTemplate> incomingReplication = new ArrayList<>();
+  private GerritMaintenanceSpecTemplate gerritMaintenance;
 
   public List<IncomingReplicationTaskTemplate> getIncomingReplication() {
     return incomingReplication;
@@ -30,9 +32,17 @@
     this.incomingReplication = incomingReplTasks;
   }
 
+  public GerritMaintenanceSpecTemplate getGerritMaintenance() {
+    return gerritMaintenance;
+  }
+
+  public void setGerritMaintenance(GerritMaintenanceSpecTemplate gerritMaintenance) {
+    this.gerritMaintenance = gerritMaintenance;
+  }
+
   @Override
   public int hashCode() {
-    return Objects.hash(incomingReplication);
+    return Objects.hash(gerritMaintenance, incomingReplication);
   }
 
   @Override
@@ -41,11 +51,16 @@
     if (obj == null) return false;
     if (getClass() != obj.getClass()) return false;
     ScheduledTasks other = (ScheduledTasks) obj;
-    return Objects.equals(incomingReplication, other.incomingReplication);
+    return Objects.equals(gerritMaintenance, other.gerritMaintenance)
+        && Objects.equals(incomingReplication, other.incomingReplication);
   }
 
   @Override
   public String toString() {
-    return "ScheduledTasks [incomingReplTasks=" + incomingReplication + "]";
+    return "ScheduledTasks [incomingReplication="
+        + incomingReplication
+        + ", gerritMaintenance="
+        + gerritMaintenance
+        + "]";
   }
 }
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/gitgc/GitGarbageCollection.java b/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/gitgc/GitGarbageCollection.java
index b41c982..10f4ea8 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/gitgc/GitGarbageCollection.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/gitgc/GitGarbageCollection.java
@@ -28,6 +28,7 @@
 @Version(Constants.VERSION)
 @ShortNames("gitgc")
 @Plural("gitgcs")
+@Deprecated
 public class GitGarbageCollection
     extends CustomResource<GitGarbageCollectionSpec, GitGarbageCollectionStatus>
     implements Namespaced {
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/gitgc/GitGarbageCollectionSpec.java b/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/gitgc/GitGarbageCollectionSpec.java
index 2c449e3..a0595f2 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/gitgc/GitGarbageCollectionSpec.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/gitgc/GitGarbageCollectionSpec.java
@@ -22,6 +22,7 @@
 import java.util.Objects;
 import java.util.Set;
 
+@Deprecated
 public class GitGarbageCollectionSpec {
   private String cluster;
   private String schedule;
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/gitgc/GitGarbageCollectionStatus.java b/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/gitgc/GitGarbageCollectionStatus.java
index 7d2b4b4..bf621ef 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/gitgc/GitGarbageCollectionStatus.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/gitgc/GitGarbageCollectionStatus.java
@@ -18,6 +18,7 @@
 import java.util.Objects;
 import java.util.Set;
 
+@Deprecated
 public class GitGarbageCollectionStatus {
   private boolean replicateAll = false;
   private Set<String> excludedProjects = new HashSet<>();
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/maintenance/GerritMaintenance.java b/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/maintenance/GerritMaintenance.java
new file mode 100644
index 0000000..1c69f44
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/maintenance/GerritMaintenance.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.api.model.maintenance;
+
+import com.google.gerrit.k8s.operator.Constants;
+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.Version;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+@Group("gerritoperator.google.com")
+@Version(Constants.VERSION)
+public class GerritMaintenance extends CustomResource<GerritMaintenanceSpec, Status>
+    implements Namespaced {
+  private static final long serialVersionUID = 1L;
+
+  public String toString() {
+    return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE);
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/maintenance/GerritMaintenanceSpec.java b/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/maintenance/GerritMaintenanceSpec.java
new file mode 100644
index 0000000..e4f7ba8
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/maintenance/GerritMaintenanceSpec.java
@@ -0,0 +1,73 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.api.model.maintenance;
+
+import com.google.gerrit.k8s.operator.api.model.shared.ContainerImageConfig;
+import com.google.gerrit.k8s.operator.api.model.shared.GerritStorageConfig;
+import java.util.Objects;
+
+public class GerritMaintenanceSpec extends GerritMaintenanceSpecTemplate {
+  private ContainerImageConfig containerImages = new ContainerImageConfig();
+  private GerritStorageConfig storage = new GerritStorageConfig();
+
+  public GerritMaintenanceSpec() {}
+
+  public GerritMaintenanceSpec(GerritMaintenanceSpecTemplate template) {
+    super(template);
+  }
+
+  public ContainerImageConfig getContainerImages() {
+    return containerImages;
+  }
+
+  public void setContainerImages(ContainerImageConfig containerImages) {
+    this.containerImages = containerImages;
+  }
+
+  public GerritStorageConfig getStorage() {
+    return storage;
+  }
+
+  public void setStorage(GerritStorageConfig storage) {
+    this.storage = storage;
+  }
+
+  @Override
+  public int hashCode() {
+    final int prime = 31;
+    int result = super.hashCode();
+    result = prime * result + Objects.hash(containerImages, storage);
+    return result;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) return true;
+    if (!super.equals(obj)) return false;
+    if (getClass() != obj.getClass()) return false;
+    GerritMaintenanceSpec other = (GerritMaintenanceSpec) obj;
+    return Objects.equals(containerImages, other.containerImages)
+        && Objects.equals(storage, other.storage);
+  }
+
+  @Override
+  public String toString() {
+    return "GerritMaintenanceSpec [containerImages="
+        + containerImages
+        + ", storage="
+        + storage
+        + "]";
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/maintenance/GerritMaintenanceSpecTemplate.java b/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/maintenance/GerritMaintenanceSpecTemplate.java
new file mode 100644
index 0000000..97ad986
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/maintenance/GerritMaintenanceSpecTemplate.java
@@ -0,0 +1,64 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.api.model.maintenance;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.google.gerrit.k8s.operator.api.model.cluster.GerritCluster;
+import java.util.Objects;
+
+public class GerritMaintenanceSpecTemplate {
+  private GerritProjectsTasks projects = new GerritProjectsTasks();
+
+  public GerritMaintenanceSpecTemplate(GerritMaintenanceSpecTemplate spec) {
+    this.projects = spec.getProjects();
+  }
+
+  public GerritMaintenanceSpecTemplate() {}
+
+  public GerritProjectsTasks getProjects() {
+    return projects;
+  }
+
+  public void setProjects(GerritProjectsTasks projects) {
+    this.projects = projects;
+  }
+
+  @JsonIgnore
+  public GerritMaintenanceSpec toGerritMaintenanceSpec(GerritCluster gerritCluster) {
+    GerritMaintenanceSpec spec = new GerritMaintenanceSpec(this);
+    spec.setContainerImages(gerritCluster.getSpec().getContainerImages());
+    spec.setStorage(gerritCluster.getSpec().getStorage());
+    return spec;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(projects);
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) return true;
+    if (obj == null) return false;
+    if (getClass() != obj.getClass()) return false;
+    GerritMaintenanceSpecTemplate other = (GerritMaintenanceSpecTemplate) obj;
+    return Objects.equals(projects, other.projects);
+  }
+
+  @Override
+  public String toString() {
+    return "GerritMaintenanceSpec [projects=" + projects + "]";
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/maintenance/GerritMaintenanceTask.java b/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/maintenance/GerritMaintenanceTask.java
new file mode 100644
index 0000000..07efc78
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/maintenance/GerritMaintenanceTask.java
@@ -0,0 +1,103 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.api.model.maintenance;
+
+import io.fabric8.kubernetes.api.model.Affinity;
+import io.fabric8.kubernetes.api.model.ResourceRequirements;
+import io.fabric8.kubernetes.api.model.Toleration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+public abstract class GerritMaintenanceTask {
+  private String name;
+  private String schedule;
+  private ResourceRequirements resources;
+  private List<Toleration> tolerations = new ArrayList<>();
+  private Affinity affinity;
+
+  public String getName() {
+    return name;
+  }
+
+  public void setName(String name) {
+    this.name = name;
+  }
+
+  public String getSchedule() {
+    return schedule;
+  }
+
+  public void setSchedule(String schedule) {
+    this.schedule = schedule;
+  }
+
+  public ResourceRequirements getResources() {
+    return resources;
+  }
+
+  public void setResources(ResourceRequirements resources) {
+    this.resources = resources;
+  }
+
+  public List<Toleration> getTolerations() {
+    return tolerations;
+  }
+
+  public void setTolerations(List<Toleration> tolerations) {
+    this.tolerations = tolerations;
+  }
+
+  public Affinity getAffinity() {
+    return affinity;
+  }
+
+  public void setAffinity(Affinity affinity) {
+    this.affinity = affinity;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(affinity, name, resources, schedule, tolerations);
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) return true;
+    if (obj == null) return false;
+    if (getClass() != obj.getClass()) return false;
+    GerritMaintenanceTask other = (GerritMaintenanceTask) obj;
+    return Objects.equals(affinity, other.affinity)
+        && Objects.equals(name, other.name)
+        && Objects.equals(resources, other.resources)
+        && Objects.equals(schedule, other.schedule)
+        && Objects.equals(tolerations, other.tolerations);
+  }
+
+  @Override
+  public String toString() {
+    return "GerritMaintenanceTask [name="
+        + name
+        + ", schedule="
+        + schedule
+        + ", resources="
+        + resources
+        + ", tolerations="
+        + tolerations
+        + ", affinity="
+        + affinity
+        + "]";
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/maintenance/GerritProjectsTask.java b/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/maintenance/GerritProjectsTask.java
new file mode 100644
index 0000000..d12daf9
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/maintenance/GerritProjectsTask.java
@@ -0,0 +1,62 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.api.model.maintenance;
+
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+public abstract class GerritProjectsTask extends GerritMaintenanceTask {
+  private Set<String> include = new HashSet<>();
+  private Set<String> exclude = new HashSet<>();
+
+  public Set<String> getInclude() {
+    return include;
+  }
+
+  public void setInclude(Set<String> include) {
+    this.include = include;
+  }
+
+  public Set<String> getExclude() {
+    return exclude;
+  }
+
+  public void setExclude(Set<String> exclude) {
+    this.exclude = exclude;
+  }
+
+  @Override
+  public int hashCode() {
+    final int prime = 31;
+    int result = super.hashCode();
+    result = prime * result + Objects.hash(exclude, include);
+    return result;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) return true;
+    if (!super.equals(obj)) return false;
+    if (getClass() != obj.getClass()) return false;
+    GerritProjectsTask other = (GerritProjectsTask) obj;
+    return Objects.equals(exclude, other.exclude) && Objects.equals(include, other.include);
+  }
+
+  @Override
+  public String toString() {
+    return "GerritProjectsTask [include=" + include + ", exclude=" + exclude + "]";
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/maintenance/GerritProjectsTasks.java b/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/maintenance/GerritProjectsTasks.java
new file mode 100644
index 0000000..4bcc325
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/maintenance/GerritProjectsTasks.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.api.model.maintenance;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+public class GerritProjectsTasks {
+  private List<GitGcTask> gc = new ArrayList<>();
+
+  public List<GitGcTask> getGc() {
+    return gc;
+  }
+
+  public void setGc(List<GitGcTask> gc) {
+    this.gc = gc;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(gc);
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) return true;
+    if (obj == null) return false;
+    if (getClass() != obj.getClass()) return false;
+    GerritProjectsTasks other = (GerritProjectsTasks) obj;
+    return Objects.equals(gc, other.gc);
+  }
+
+  @Override
+  public String toString() {
+    return "GerritProjectsTasks [gc=" + gc + "]";
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/maintenance/GitGcTask.java b/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/maintenance/GitGcTask.java
new file mode 100644
index 0000000..198608f
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/api/model/maintenance/GitGcTask.java
@@ -0,0 +1,62 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.api.model.maintenance;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+public class GitGcTask extends GerritProjectsTask {
+  private List<String> args = new ArrayList<>();
+  private String gitOptions;
+
+  public List<String> getArgs() {
+    return args;
+  }
+
+  public void setArgs(List<String> args) {
+    this.args = args;
+  }
+
+  public String getGitOptions() {
+    return gitOptions;
+  }
+
+  public void setGitOptions(String gitOptions) {
+    this.gitOptions = gitOptions;
+  }
+
+  @Override
+  public int hashCode() {
+    final int prime = 31;
+    int result = super.hashCode();
+    result = prime * result + Objects.hash(args, gitOptions);
+    return result;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) return true;
+    if (!super.equals(obj)) return false;
+    if (getClass() != obj.getClass()) return false;
+    GitGcTask other = (GitGcTask) obj;
+    return Objects.equals(args, other.args) && Objects.equals(gitOptions, other.gitOptions);
+  }
+
+  @Override
+  public String toString() {
+    return "GitGcTask [args=" + args + ", gitOptions=" + gitOptions + "]";
+  }
+}
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 587c53f..7123d2f 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,6 +15,7 @@
 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_MAINTENANCE_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_INC_REPL_TASK_EVENT_SOURCE;
 import static com.google.gerrit.k8s.operator.cluster.GerritClusterReconciler.CLUSTER_MANAGED_RECEIVER_EVENT_SOURCE;
@@ -25,12 +26,14 @@
 import com.google.gerrit.k8s.operator.api.model.cluster.GerritClusterStatus;
 import com.google.gerrit.k8s.operator.api.model.gerrit.Gerrit;
 import com.google.gerrit.k8s.operator.api.model.gerrit.GerritTemplate;
+import com.google.gerrit.k8s.operator.api.model.maintenance.GerritMaintenance;
 import com.google.gerrit.k8s.operator.api.model.network.GerritNetwork;
 import com.google.gerrit.k8s.operator.api.model.receiver.Receiver;
 import com.google.gerrit.k8s.operator.api.model.receiver.ReceiverTemplate;
 import com.google.gerrit.k8s.operator.api.model.tasks.incomingrepl.IncomingReplicationTask;
 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.ClusterManagedGerritMaintenance;
 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.ClusterManagedIncomingReplicationTask;
@@ -87,6 +90,9 @@
       @Dependent(
           type = ClusterManagedIncomingReplicationTask.class,
           useEventSourceWithName = CLUSTER_MANAGED_INC_REPL_TASK_EVENT_SOURCE),
+      @Dependent(
+          type = ClusterManagedGerritMaintenance.class,
+          useEventSourceWithName = CLUSTER_MANAGED_GERRIT_MAINTENANCE_EVENT_SOURCE),
     })
 public class GerritClusterReconciler
     implements Reconciler<GerritCluster>, EventSourceInitializer<GerritCluster> {
@@ -98,6 +104,8 @@
       "cluster-managed-gerrit-network";
   public static final String CLUSTER_MANAGED_INC_REPL_TASK_EVENT_SOURCE =
       "cluster-managed-incoming-repl-task";
+  public static final String CLUSTER_MANAGED_GERRIT_MAINTENANCE_EVENT_SOURCE =
+      "cluster-managed-gerrit-maintenance";
 
   @Override
   public Map<String, EventSource> prepareEventSources(EventSourceContext<GerritCluster> context) {
@@ -127,6 +135,11 @@
                 InformerConfiguration.from(IncomingReplicationTask.class, context).build(),
                 context);
 
+    InformerEventSource<GerritMaintenance, GerritCluster>
+        clusterManagedGerritMaintenanceEventSource =
+            new InformerEventSource<>(
+                InformerConfiguration.from(GerritMaintenance.class, context).build(), context);
+
     Map<String, EventSource> eventSources = new HashMap<>();
     eventSources.put(CM_EVENT_SOURCE, cmEventSource);
     eventSources.put(PVC_EVENT_SOURCE, pvcEventSource);
@@ -136,6 +149,9 @@
         CLUSTER_MANAGED_GERRIT_NETWORK_EVENT_SOURCE, clusterManagedGerritNetworkEventSource);
     eventSources.put(
         CLUSTER_MANAGED_INC_REPL_TASK_EVENT_SOURCE, clusterManagedIncomingReplTaskEventSource);
+    eventSources.put(
+        CLUSTER_MANAGED_GERRIT_MAINTENANCE_EVENT_SOURCE,
+        clusterManagedGerritMaintenanceEventSource);
     return eventSources;
   }
 
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerritMaintenance.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerritMaintenance.java
new file mode 100644
index 0000000..46c9a96
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerritMaintenance.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 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.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.api.model.maintenance.GerritMaintenance;
+import com.google.gerrit.k8s.operator.util.KubernetesDependentCustomResource;
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+
+@KubernetesDependent
+public class ClusterManagedGerritMaintenance
+    extends KubernetesDependentCustomResource<GerritMaintenance, GerritCluster> {
+  public static final String NAME_SUFFIX = "gerrit-maintenance";
+
+  public ClusterManagedGerritMaintenance() {
+    super(GerritMaintenance.class);
+  }
+
+  @Override
+  public GerritMaintenance desired(GerritCluster gerritCluster, Context<GerritCluster> context) {
+    GerritMaintenance maintenance = new GerritMaintenance();
+    maintenance.setMetadata(
+        new ObjectMetaBuilder()
+            .withName(String.format("%s-%s", gerritCluster.getMetadata().getName(), NAME_SUFFIX))
+            .withNamespace(gerritCluster.getMetadata().getNamespace())
+            .build());
+    maintenance.setSpec(
+        gerritCluster
+            .getSpec()
+            .getScheduledTasks()
+            .getGerritMaintenance()
+            .toGerritMaintenanceSpec(gerritCluster));
+    return maintenance;
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerritMaintenanceCondition.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerritMaintenanceCondition.java
new file mode 100644
index 0000000..3fb2f16
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerritMaintenanceCondition.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 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.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.api.model.maintenance.GerritMaintenance;
+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 ClusterManagedGerritMaintenanceCondition
+    implements Condition<GerritMaintenance, GerritCluster> {
+
+  @Override
+  public boolean isMet(
+      DependentResource<GerritMaintenance, GerritCluster> dependentResource,
+      GerritCluster gerritCluster,
+      Context<GerritCluster> context) {
+    return gerritCluster.getSpec().getScheduledTasks().getGerritMaintenance() != null;
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollectionReconciler.java b/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollectionReconciler.java
index a2eabbc..39182d0 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollectionReconciler.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollectionReconciler.java
@@ -42,6 +42,7 @@
 
 @Singleton
 @ControllerConfiguration
+@Deprecated
 public class GitGarbageCollectionReconciler
     implements Reconciler<GitGarbageCollection>,
         EventSourceInitializer<GitGarbageCollection>,
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 c8ee9bb..4be93e6 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
@@ -34,6 +34,7 @@
 import java.util.List;
 import java.util.Map;
 
+@Deprecated
 public class GitGarbageCollectionCronJob
     extends CRUDReconcileAddKubernetesDependentResource<CronJob, GitGarbageCollection> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/maintenance/GerritMaintenanceReconciler.java b/operator/src/main/java/com/google/gerrit/k8s/operator/maintenance/GerritMaintenanceReconciler.java
new file mode 100644
index 0000000..8b95011
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/maintenance/GerritMaintenanceReconciler.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.maintenance;
+
+import com.google.gerrit.k8s.operator.api.model.maintenance.GerritMaintenance;
+import com.google.gerrit.k8s.operator.maintenance.dependent.GerritMaintenanceCronJobs;
+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-maintenance-cronjobs", type = GerritMaintenanceCronJobs.class)
+    })
+public class GerritMaintenanceReconciler implements Reconciler<GerritMaintenance> {
+
+  @Override
+  public UpdateControl<GerritMaintenance> reconcile(
+      GerritMaintenance gerritMaintenance, Context<GerritMaintenance> context) {
+    return UpdateControl.noUpdate();
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/maintenance/dependent/GerritMaintenanceCronJobs.java b/operator/src/main/java/com/google/gerrit/k8s/operator/maintenance/dependent/GerritMaintenanceCronJobs.java
new file mode 100644
index 0000000..901ac2a
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/maintenance/dependent/GerritMaintenanceCronJobs.java
@@ -0,0 +1,92 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.maintenance.dependent;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.k8s.operator.api.model.maintenance.GerritMaintenance;
+import com.google.gerrit.k8s.operator.api.model.maintenance.GerritProjectsTasks;
+import com.google.gerrit.k8s.operator.api.model.maintenance.GitGcTask;
+import com.google.gerrit.k8s.operator.util.KubernetesDependentCustomResource;
+import io.fabric8.kubernetes.api.model.batch.v1.CronJob;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.BulkDependentResource;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+public class GerritMaintenanceCronJobs
+    extends KubernetesDependentCustomResource<CronJob, GerritMaintenance>
+    implements BulkDependentResource<CronJob, GerritMaintenance> {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  public GerritMaintenanceCronJobs() {
+    super(CronJob.class);
+  }
+
+  @Override
+  public Map<String, CronJob> desiredResources(
+      GerritMaintenance primary, Context<GerritMaintenance> context) {
+    Map<String, CronJob> cronJobs = new HashMap<>();
+    GerritProjectsTasks projectsTasks = primary.getSpec().getProjects();
+    if (projectsTasks != null) {
+      List<GitGcTask> gcTasks = List.copyOf(projectsTasks.getGc());
+      checkForConflict(gcTasks);
+      for (GitGcTask gcTask : gcTasks) {
+        cronJobs.put(
+            gcTask.getName(), GitGarbageCollectionCronJob.desired(primary, gcTask, context));
+      }
+    }
+    return cronJobs;
+  }
+
+  @Override
+  public Map<String, CronJob> getSecondaryResources(
+      GerritMaintenance primary, Context<GerritMaintenance> context) {
+    Set<CronJob> cronjobs = context.getSecondaryResources(CronJob.class);
+    Map<String, CronJob> result = new HashMap<>(cronjobs.size());
+    for (CronJob cj : cronjobs) {
+      result.put(cj.getMetadata().getName(), cj);
+    }
+    return result;
+  }
+
+  public static void checkForConflict(List<GitGcTask> gcTasks) {
+    logger.atFine().log("Checking for conflicts in Git GC tasks");
+    if (gcTasks.stream().filter(gc -> gc.getInclude().isEmpty()).count() > 1) {
+      throw new GerritMaintenanceTaskConflictException(
+          "Only a single GitGc working on all projects allowed per GerritMaintenance.");
+    }
+
+    Set<String> projectsWithMultipleGcTasks =
+        gcTasks.stream()
+            .map(gc -> gc.getInclude())
+            .flatMap(Collection::stream)
+            .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
+            .entrySet()
+            .stream()
+            .filter(e -> e.getValue() > 1)
+            .map(e -> e.getKey())
+            .collect(Collectors.toSet());
+    if (!projectsWithMultipleGcTasks.isEmpty()) {
+      throw new GerritMaintenanceTaskConflictException(
+          "Only a single Git GC task allowed per project. Projects with conflicts: "
+              + String.join(", ", projectsWithMultipleGcTasks));
+    }
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/maintenance/dependent/GerritMaintenanceTaskConflictException.java b/operator/src/main/java/com/google/gerrit/k8s/operator/maintenance/dependent/GerritMaintenanceTaskConflictException.java
new file mode 100644
index 0000000..0d45168
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/maintenance/dependent/GerritMaintenanceTaskConflictException.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.maintenance.dependent;
+
+public class GerritMaintenanceTaskConflictException extends RuntimeException {
+  private static final long serialVersionUID = 1L;
+
+  public GerritMaintenanceTaskConflictException(String message) {
+    super(message);
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/maintenance/dependent/GitGarbageCollectionCronJob.java b/operator/src/main/java/com/google/gerrit/k8s/operator/maintenance/dependent/GitGarbageCollectionCronJob.java
new file mode 100644
index 0000000..3a50efe
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/maintenance/dependent/GitGarbageCollectionCronJob.java
@@ -0,0 +1,251 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.maintenance.dependent;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.k8s.operator.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.api.model.maintenance.GerritMaintenance;
+import com.google.gerrit.k8s.operator.api.model.maintenance.GitGcTask;
+import com.google.gerrit.k8s.operator.api.model.shared.ContainerImageConfig;
+import com.google.gerrit.k8s.operator.api.model.shared.NfsWorkaroundConfig;
+import com.google.gerrit.k8s.operator.cluster.GerritClusterLabelFactory;
+import com.google.gerrit.k8s.operator.components.GerritSecurityContext;
+import com.google.gerrit.k8s.operator.maintenance.GerritMaintenanceReconciler;
+import io.fabric8.kubernetes.api.model.Container;
+import io.fabric8.kubernetes.api.model.ContainerBuilder;
+import io.fabric8.kubernetes.api.model.Volume;
+import io.fabric8.kubernetes.api.model.VolumeMount;
+import io.fabric8.kubernetes.api.model.batch.v1.CronJob;
+import io.fabric8.kubernetes.api.model.batch.v1.CronJobBuilder;
+import io.fabric8.kubernetes.api.model.batch.v1.JobTemplateSpec;
+import io.fabric8.kubernetes.api.model.batch.v1.JobTemplateSpecBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+
+public class GitGarbageCollectionCronJob {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  protected static CronJob desired(
+      GerritMaintenance gerritMaintenance,
+      GitGcTask gitGcTask,
+      Context<GerritMaintenance> context) {
+    String ns = gerritMaintenance.getMetadata().getNamespace();
+    String name = gitGcTask.getName();
+    logger.atInfo().log("Reconciling GitGcTask with name: %s/%s", ns, name);
+
+    Map<String, String> gitGcLabels = getLabels(gerritMaintenance, gitGcTask.getName());
+    List<Container> initContainers = new ArrayList<>();
+    NfsWorkaroundConfig nfsWorkaround =
+        gerritMaintenance.getSpec().getStorage().getStorageClasses().getNfsWorkaround();
+    if (nfsWorkaround.isEnabled() && nfsWorkaround.isChownOnStartup()) {
+      boolean hasIdmapdConfig =
+          gerritMaintenance
+                  .getSpec()
+                  .getStorage()
+                  .getStorageClasses()
+                  .getNfsWorkaround()
+                  .getIdmapdConfig()
+              != null;
+      ContainerImageConfig images = gerritMaintenance.getSpec().getContainerImages();
+      initContainers.add(GerritCluster.createNfsInitContainer(hasIdmapdConfig, images));
+    }
+
+    JobTemplateSpec gitGcJobTemplate =
+        new JobTemplateSpecBuilder()
+            .withNewSpec()
+            .withNewTemplate()
+            .withNewMetadata()
+            .withAnnotations(
+                Map.of(
+                    "sidecar.istio.io/inject",
+                    "false",
+                    "cluster-autoscaler.kubernetes.io/safe-to-evict",
+                    "false"))
+            .withLabels(gitGcLabels)
+            .endMetadata()
+            .withNewSpec()
+            .withTolerations(gitGcTask.getTolerations())
+            .withAffinity(gitGcTask.getAffinity())
+            .addAllToImagePullSecrets(
+                gerritMaintenance.getSpec().getContainerImages().getImagePullSecrets())
+            .withRestartPolicy("OnFailure")
+            .withSecurityContext(GerritSecurityContext.forPod())
+            .addAllToInitContainers(initContainers)
+            .addToContainers(buildGitGcContainer(gerritMaintenance, gitGcTask))
+            .withVolumes(getVolumes(gerritMaintenance))
+            .endSpec()
+            .endTemplate()
+            .endSpec()
+            .build();
+
+    return new CronJobBuilder()
+        .withApiVersion("batch/v1")
+        .withNewMetadata()
+        .withNamespace(ns)
+        .withName(name)
+        .withLabels(gitGcLabels)
+        .endMetadata()
+        .withNewSpec()
+        .withSchedule(gitGcTask.getSchedule())
+        .withConcurrencyPolicy("Forbid")
+        .withJobTemplate(gitGcJobTemplate)
+        .endSpec()
+        .build();
+  }
+
+  private static Container buildGitGcContainer(
+      GerritMaintenance gerritMaintenance, GitGcTask gitGcTask) {
+    List<VolumeMount> volumeMounts = new ArrayList<>();
+    volumeMounts.add(GerritCluster.getGitRepositoriesVolumeMount("/var/gerrit/git"));
+
+    if (gerritMaintenance.getSpec().getStorage().getStorageClasses().getNfsWorkaround().isEnabled()
+        && gerritMaintenance
+                .getSpec()
+                .getStorage()
+                .getStorageClasses()
+                .getNfsWorkaround()
+                .getIdmapdConfig()
+            != null) {
+      volumeMounts.add(GerritCluster.getNfsImapdConfigVolumeMount());
+    }
+
+    ContainerBuilder gitGcContainerBuilder =
+        new ContainerBuilder()
+            .withName("git-gc")
+            .withSecurityContext(GerritSecurityContext.forContainer())
+            .withImagePullPolicy(
+                gerritMaintenance.getSpec().getContainerImages().getImagePullPolicy())
+            .withImage(
+                gerritMaintenance
+                    .getSpec()
+                    .getContainerImages()
+                    .getGerritImages()
+                    .getFullImageName("gerrit-maintenance"))
+            .withResources(gitGcTask.getResources())
+            .withVolumeMounts(volumeMounts);
+
+    ArrayList<String> args = new ArrayList<>();
+    args.add("-d");
+    args.add("/var/gerrit");
+    args.add("projects");
+    for (String project : gitGcTask.getInclude()) {
+      args.add("--project");
+      args.add(project);
+    }
+    for (String project : getExcludedProjects(gerritMaintenance, gitGcTask)) {
+      args.add("--skip");
+      args.add(project);
+    }
+    args.add("gc");
+    String gitOpts = gitGcTask.getGitOptions();
+    if (gitOpts != null && !gitOpts.isBlank()) {
+      args.addAll(parseGitConfigOptsFromFile(gitGcTask.getGitOptions()));
+    }
+    args.addAll(gitGcTask.getArgs());
+    gitGcContainerBuilder.addAllToArgs(args);
+
+    return gitGcContainerBuilder.build();
+  }
+
+  private static List<Volume> getVolumes(GerritMaintenance gerritMaintenance) {
+    List<Volume> volumes = new ArrayList<>();
+
+    volumes.add(
+        GerritCluster.getSharedVolume(
+            gerritMaintenance.getSpec().getStorage().getSharedStorage().getExternalPVC()));
+
+    if (gerritMaintenance
+        .getSpec()
+        .getStorage()
+        .getStorageClasses()
+        .getNfsWorkaround()
+        .isEnabled()) {
+      if (gerritMaintenance
+              .getSpec()
+              .getStorage()
+              .getStorageClasses()
+              .getNfsWorkaround()
+              .getIdmapdConfig()
+          != null) {
+        volumes.add(GerritCluster.getNfsImapdConfigVolume());
+      }
+    }
+    return volumes;
+  }
+
+  private static List<String> parseGitConfigOptsFromFile(String configFile) {
+    List<String> opts = new ArrayList<>();
+    Config cfg = new Config();
+    try {
+      cfg.fromText(configFile);
+      for (String section : cfg.getSections()) {
+        Set<String> subsections = cfg.getSubsections(section);
+        if (subsections.isEmpty()) {
+          for (String name : cfg.getNames(section)) {
+            opts.add(formatGitCfgOption(section, null, name, cfg.getString(section, null, name)));
+          }
+        }
+        for (String subsection : cfg.getSubsections(section)) {
+          for (String name : cfg.getNames(section, subsection)) {
+            opts.add(
+                formatGitCfgOption(
+                    section, subsection, name, cfg.getString(section, subsection, name)));
+          }
+        }
+      }
+    } catch (ConfigInvalidException e) {
+      throw new IllegalStateException("Invalid git config in Git GC task.", e);
+    }
+    return opts;
+  }
+
+  private static String formatGitCfgOption(
+      String section, String subsection, String name, String value) {
+    if (subsection == null || subsection.isBlank()) {
+      return String.format("-c %s.%s=%s", section, name, value);
+    }
+    return String.format("-c %s.%s.%s=%s", section, subsection, name, value);
+  }
+
+  private static String getComponentName(GerritMaintenance primary) {
+    return String.format("gerrit-maintenance-%s", primary.getMetadata().getName());
+  }
+
+  private static Map<String, String> getLabels(GerritMaintenance primary, String taskName) {
+    String name = String.format("git-gc-%s", taskName);
+    return GerritClusterLabelFactory.create(
+        name, getComponentName(primary), GerritMaintenanceReconciler.class.getSimpleName());
+  }
+
+  private static Set<String> getExcludedProjects(
+      GerritMaintenance gerritMaintenance, GitGcTask gcTask) {
+    Set<String> excludedProjects = gcTask.getExclude();
+
+    if (gcTask.getInclude().isEmpty()) {
+      List<GitGcTask> gitGcs = gerritMaintenance.getSpec().getProjects().getGc();
+      gitGcs.remove(gcTask);
+      for (GitGcTask gc : gitGcs) {
+        excludedProjects.addAll(gc.getInclude());
+      }
+    }
+
+    return excludedProjects;
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/server/ServerModule.java b/operator/src/main/java/com/google/gerrit/k8s/operator/server/ServerModule.java
index 2043179..ff7e9db 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/server/ServerModule.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/server/ServerModule.java
@@ -18,6 +18,7 @@
 
 import com.google.gerrit.k8s.operator.admission.servlet.GerritAdmissionWebhook;
 import com.google.gerrit.k8s.operator.admission.servlet.GerritClusterAdmissionWebhook;
+import com.google.gerrit.k8s.operator.admission.servlet.GerritMaintenanceAdmissionWebhook;
 import com.google.gerrit.k8s.operator.admission.servlet.GitGcAdmissionWebhook;
 import com.google.inject.AbstractModule;
 import com.google.inject.multibindings.Multibinder;
@@ -36,5 +37,6 @@
     admissionWebhookServlets.addBinding().to(GerritClusterAdmissionWebhook.class);
     admissionWebhookServlets.addBinding().to(GitGcAdmissionWebhook.class);
     admissionWebhookServlets.addBinding().to(GerritAdmissionWebhook.class);
+    admissionWebhookServlets.addBinding().to(GerritMaintenanceAdmissionWebhook.class);
   }
 }
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 e9714a5..6d22d25 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
@@ -4,3 +4,4 @@
 com.google.gerrit.k8s.operator.api.model.tasks.incomingrepl.IncomingReplicationTask
 com.google.gerrit.k8s.operator.api.model.receiver.Receiver
 com.google.gerrit.k8s.operator.api.model.network.GerritNetwork
+com.google.gerrit.k8s.operator.api.model.maintenance.GerritMaintenance
\ No newline at end of file
diff --git a/operator/src/test/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritClusterGerritsTest.java b/operator/src/test/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritClusterGerritsTest.java
index d0fef72..f28a30b 100644
--- a/operator/src/test/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritClusterGerritsTest.java
+++ b/operator/src/test/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritClusterGerritsTest.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.k8s.operator.OperatorContext;
 import com.google.gerrit.k8s.operator.api.model.cluster.GerritCluster;
 import com.google.gerrit.k8s.operator.api.model.gerrit.Gerrit;
+import com.google.gerrit.k8s.operator.api.model.maintenance.GerritMaintenance;
 import com.google.gerrit.k8s.operator.gerrit.dependent.GerritConfigMap;
 import io.fabric8.kubernetes.api.model.ConfigMap;
 import io.javaoperatorsdk.operator.ReconcilerUtils;
@@ -37,7 +38,8 @@
   public void expectedGerritClusterGerritsCreated(
       String inputFile,
       String expectedGerritPrimaryOutputFile,
-      String expectedGerritReplicaOutputFile)
+      String expectedGerritReplicaOutputFile,
+      String expectedGerritMaintenanceOutputFile)
       throws ConfigInvalidException {
 
     OperatorContext.createInstance(Constants.ClusterMode.HIGH_AVAILABILITY);
@@ -77,11 +79,21 @@
         default:
       }
     }
+    GerritMaintenance expectedGerritMaintenance =
+        ReconcilerUtils.loadYaml(
+            GerritMaintenance.class, this.getClass(), expectedGerritMaintenanceOutputFile);
+    ClusterManagedGerritMaintenance gerritMaintenanceReconciler =
+        new ClusterManagedGerritMaintenance();
+    assertThat(gerritMaintenanceReconciler.desired(gerritCluster, null))
+        .isEqualTo(expectedGerritMaintenance);
   }
 
   private static Stream<Arguments> provideYamlManifests() {
     return Stream.of(
         Arguments.of(
-            "../gerritcluster_primary_replica.yaml", "gerrit_primary.yaml", "gerrit_replica.yaml"));
+            "../gerritcluster_primary_replica.yaml",
+            "gerrit_primary.yaml",
+            "gerrit_replica.yaml",
+            "gerrit_maintenance.yaml"));
   }
 }
diff --git a/operator/src/test/java/com/google/gerrit/k8s/operator/maintenance/GerritMaintenanceGitGcE2E.java b/operator/src/test/java/com/google/gerrit/k8s/operator/maintenance/GerritMaintenanceGitGcE2E.java
new file mode 100644
index 0000000..53b2687
--- /dev/null
+++ b/operator/src/test/java/com/google/gerrit/k8s/operator/maintenance/GerritMaintenanceGitGcE2E.java
@@ -0,0 +1,213 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.maintenance;
+
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.k8s.operator.api.model.maintenance.GerritMaintenance;
+import com.google.gerrit.k8s.operator.network.IngressType;
+import com.google.gerrit.k8s.operator.test.AbstractGerritOperatorE2ETest;
+import io.fabric8.kubernetes.api.model.batch.v1.CronJob;
+import io.fabric8.kubernetes.api.model.batch.v1.Job;
+import java.util.List;
+import java.util.Set;
+import org.junit.jupiter.api.Test;
+
+public class GerritMaintenanceGitGcE2E extends AbstractGerritOperatorE2ETest {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  static final String GITGC_SCHEDULE = "*/1 * * * *";
+
+  @Test
+  void testGitGcAllProjectsCreationAndDeletion() {
+    GerritMaintenance gm =
+        GerritMaintenanceTestHelper.createGerritMaintenanceWithGitGcs(
+            operator.getNamespace(), List.of(Set.of()));
+    client.resource(gm).createOrReplace();
+    logger.atInfo().log("Waiting max 2 minutes for Git Gc Cronjob to be created.");
+    String gcCronJobName = gm.getSpec().getProjects().getGc().get(0).getName();
+    await()
+        .atMost(2, MINUTES)
+        .untilAsserted(
+            () -> {
+              assertGerritMaintenanceCreation(gm.getMetadata().getName());
+              assertGitGcCronJobCreation(gcCronJobName);
+              assertGitGcJobCreation(gcCronJobName);
+            });
+
+    logger.atInfo().log("Deleting test GitMaintenance object: %s", gm);
+    client.resource(gm).delete();
+    awaitGitGcDeletionAssertion(gm.getMetadata().getName(), gcCronJobName);
+  }
+
+  @Test
+  void testGitGcSelectedProjects() {
+    GerritMaintenance gm =
+        GerritMaintenanceTestHelper.createGerritMaintenanceWithGitGcs(
+            operator.getNamespace(), List.of(Set.of("All-Projects", "test")));
+
+    logger.atInfo().log("Waiting max 2 minutes for GerritMaintenance to be created.");
+    String gcCronJobName = gm.getSpec().getProjects().getGc().get(0).getName();
+    await()
+        .atMost(2, MINUTES)
+        .untilAsserted(
+            () -> {
+              assertGerritMaintenanceCreation(gm.getMetadata().getName());
+              assertGitGcCronJobCreation(gcCronJobName);
+              assertGitGcJobCreation(gcCronJobName);
+            });
+
+    client.resource(gm).delete();
+  }
+
+  @Test
+  void testSelectiveGcIsExcludedFromCompleteGc() {
+    Set<String> selectedProjects = Set.of("All-Projects", "test");
+    GerritMaintenance gm =
+        GerritMaintenanceTestHelper.createGerritMaintenanceWithGitGcs(
+            operator.getNamespace(), List.of(Set.of(), selectedProjects));
+    String gmName = gm.getMetadata().getName();
+
+    logger.atInfo().log("Waiting max 2 minutes for GerritMaintenance to be created.");
+    String completeGcCronJobName = gm.getSpec().getProjects().getGc().get(0).getName();
+    String selectedGcCronJobName = gm.getSpec().getProjects().getGc().get(1).getName();
+    await()
+        .atMost(2, MINUTES)
+        .untilAsserted(
+            () -> {
+              assertGerritMaintenanceCreation(gmName);
+              assertGitGcCronJobCreation(completeGcCronJobName);
+              assertGitGcCronJobCreation(selectedGcCronJobName);
+            });
+
+    await()
+        .atMost(2, MINUTES)
+        .untilAsserted(
+            () -> {
+              CronJob updatedCompleteGitGc =
+                  client
+                      .resources(CronJob.class)
+                      .inNamespace(operator.getNamespace())
+                      .withName(completeGcCronJobName)
+                      .get();
+              assert updatedCompleteGitGc
+                  .getSpec()
+                  .getJobTemplate()
+                  .getSpec()
+                  .getTemplate()
+                  .getSpec()
+                  .getContainers()
+                  .get(0)
+                  .getArgs()
+                  .containsAll(selectedProjects);
+            });
+
+    gm =
+        GerritMaintenanceTestHelper.createGerritMaintenanceWithGitGcs(
+            operator.getNamespace(), List.of(Set.of()));
+    await()
+        .atMost(2, MINUTES)
+        .untilAsserted(
+            () -> {
+              assertNull(
+                  client
+                      .resources(CronJob.class)
+                      .inNamespace(operator.getNamespace())
+                      .withName(selectedGcCronJobName)
+                      .get());
+              CronJob updatedCompleteGitGc =
+                  client
+                      .resources(CronJob.class)
+                      .inNamespace(operator.getNamespace())
+                      .withName(completeGcCronJobName)
+                      .get();
+              assert updatedCompleteGitGc
+                  .getSpec()
+                  .getJobTemplate()
+                  .getSpec()
+                  .getTemplate()
+                  .getSpec()
+                  .getContainers()
+                  .get(0)
+                  .getArgs()
+                  .isEmpty();
+            });
+  }
+
+  private void assertGerritMaintenanceCreation(String gmName) {
+    GerritMaintenance updatedGm =
+        client
+            .resources(GerritMaintenance.class)
+            .inNamespace(operator.getNamespace())
+            .withName(gmName)
+            .get();
+    assertThat(updatedGm, is(notNullValue()));
+  }
+
+  private void assertGitGcCronJobCreation(String gitGcName) {
+    CronJob cronJob =
+        client
+            .batch()
+            .v1()
+            .cronjobs()
+            .inNamespace(operator.getNamespace())
+            .withName(gitGcName)
+            .get();
+    assertThat(cronJob, is(notNullValue()));
+  }
+
+  private void awaitGitGcDeletionAssertion(String gmName, String gitGcName) {
+    logger.atInfo().log("Waiting max 2 minutes for GerritMaintenance to be deleted.");
+    await()
+        .atMost(2, MINUTES)
+        .untilAsserted(
+            () -> {
+              GerritMaintenance updatedGm =
+                  client
+                      .resources(GerritMaintenance.class)
+                      .inNamespace(operator.getNamespace())
+                      .withName(gmName)
+                      .get();
+              assertNull(updatedGm);
+
+              CronJob cronJob =
+                  client
+                      .batch()
+                      .v1()
+                      .cronjobs()
+                      .inNamespace(operator.getNamespace())
+                      .withName(gitGcName)
+                      .get();
+              assertNull(cronJob);
+            });
+  }
+
+  private void assertGitGcJobCreation(String gitGcName) {
+    List<Job> jobRuns =
+        client.batch().v1().jobs().inNamespace(operator.getNamespace()).list().getItems();
+    assert (jobRuns.size() > 0);
+    assert (jobRuns.get(0).getMetadata().getName().startsWith(gitGcName));
+  }
+
+  @Override
+  protected IngressType getIngressType() {
+    return IngressType.NONE;
+  }
+}
diff --git a/operator/src/test/java/com/google/gerrit/k8s/operator/maintenance/GerritMaintenanceTestHelper.java b/operator/src/test/java/com/google/gerrit/k8s/operator/maintenance/GerritMaintenanceTestHelper.java
new file mode 100644
index 0000000..34a131f
--- /dev/null
+++ b/operator/src/test/java/com/google/gerrit/k8s/operator/maintenance/GerritMaintenanceTestHelper.java
@@ -0,0 +1,49 @@
+package com.google.gerrit.k8s.operator.maintenance;
+
+import com.google.gerrit.k8s.operator.api.model.maintenance.GerritMaintenance;
+import com.google.gerrit.k8s.operator.api.model.maintenance.GerritMaintenanceSpec;
+import com.google.gerrit.k8s.operator.api.model.maintenance.GerritProjectsTasks;
+import com.google.gerrit.k8s.operator.api.model.maintenance.GitGcTask;
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+import org.apache.commons.lang3.RandomStringUtils;
+
+public class GerritMaintenanceTestHelper {
+  public static GerritMaintenance createGerritMaintenanceWithGitGcs(
+      String namespace, List<Set<String>> projectSets) {
+    GerritMaintenanceSpec spec = new GerritMaintenanceSpec();
+    GerritProjectsTasks projectsTasks = new GerritProjectsTasks();
+    List<GitGcTask> gcTasks = new ArrayList<>();
+    for (Set<String> projects : projectSets) {
+      GitGcTask gitGc = new GitGcTask();
+      gitGc.setInclude(projects);
+      gcTasks.add(gitGc);
+    }
+    projectsTasks.setGc(gcTasks);
+    spec.setProjects(projectsTasks);
+
+    GerritMaintenance gm = new GerritMaintenance();
+    gm.setMetadata(
+        new ObjectMetaBuilder()
+            .withName(RandomStringUtils.randomAlphabetic(10))
+            .withNamespace(namespace)
+            .build());
+    gm.setSpec(spec);
+    return gm;
+  }
+}
diff --git a/operator/src/test/java/com/google/gerrit/k8s/operator/maintenance/dependent/GerritMaintenanceTest.java b/operator/src/test/java/com/google/gerrit/k8s/operator/maintenance/dependent/GerritMaintenanceTest.java
new file mode 100644
index 0000000..cc7268a
--- /dev/null
+++ b/operator/src/test/java/com/google/gerrit/k8s/operator/maintenance/dependent/GerritMaintenanceTest.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.maintenance.dependent;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.k8s.operator.api.model.maintenance.GerritMaintenance;
+import io.fabric8.kubernetes.api.model.batch.v1.CronJob;
+import io.javaoperatorsdk.operator.ReconcilerUtils;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestInstance.Lifecycle;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+@TestInstance(Lifecycle.PER_CLASS)
+public class GerritMaintenanceTest {
+  @ParameterizedTest
+  @MethodSource("provideYamlManifests")
+  public void expectedCronJobCreated(String inputFile, List<String> expectedOutputFiles) {
+    GerritMaintenance input =
+        ReconcilerUtils.loadYaml(GerritMaintenance.class, this.getClass(), inputFile);
+
+    List<CronJob> expected = new ArrayList<>();
+    for (String file : expectedOutputFiles) {
+      expected.add(ReconcilerUtils.loadYaml(CronJob.class, this.getClass(), file));
+    }
+    Map<String, CronJob> cronJobs = new GerritMaintenanceCronJobs().desiredResources(input, null);
+
+    for (CronJob expectedCronJob : expected) {
+      assertThat(cronJobs.get(expectedCronJob.getMetadata().getName())).isEqualTo(expectedCronJob);
+    }
+  }
+
+  private static Stream<Arguments> provideYamlManifests() {
+    return Stream.of(
+        Arguments.of(
+            "../gerrit-maintenance.yaml",
+            List.of("cronjob_gitgc_all.yaml", "cronjob_gitgc_selected.yaml")));
+  }
+}
diff --git a/operator/src/test/java/com/google/gerrit/k8s/operator/server/GerritMaintenanceAdmissionWebhookTest.java b/operator/src/test/java/com/google/gerrit/k8s/operator/server/GerritMaintenanceAdmissionWebhookTest.java
new file mode 100644
index 0000000..70e1d5a
--- /dev/null
+++ b/operator/src/test/java/com/google/gerrit/k8s/operator/server/GerritMaintenanceAdmissionWebhookTest.java
@@ -0,0 +1,171 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.server;
+
+import static com.google.gerrit.k8s.operator.test.TestAdmissionWebhookServer.PORT;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.gerrit.k8s.operator.Constants;
+import com.google.gerrit.k8s.operator.admission.servlet.GerritMaintenanceAdmissionWebhook;
+import com.google.gerrit.k8s.operator.api.model.maintenance.GerritMaintenance;
+import com.google.gerrit.k8s.operator.maintenance.GerritMaintenanceTestHelper;
+import com.google.gerrit.k8s.operator.test.TestAdmissionWebhookServer;
+import io.fabric8.kubernetes.api.model.admission.v1.AdmissionRequest;
+import io.fabric8.kubernetes.api.model.admission.v1.AdmissionReview;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.List;
+import java.util.Set;
+import org.eclipse.jetty.http.HttpMethod;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestInstance.Lifecycle;
+
+@TestInstance(Lifecycle.PER_CLASS)
+public class GerritMaintenanceAdmissionWebhookTest {
+  private static final String NAMESPACE = "test";
+  private TestAdmissionWebhookServer server;
+
+  @BeforeAll
+  public void setup() throws Exception {
+    server = new TestAdmissionWebhookServer();
+    GerritMaintenanceAdmissionWebhook webhook = new GerritMaintenanceAdmissionWebhook();
+    server.registerWebhook(webhook);
+    server.start();
+  }
+
+  @Test
+  @DisplayName("Only a single GitGC that works on all projects in site is allowed.")
+  public void testOnlySingleGitGcWorkingOnAllProjectsIsAllowed() throws Exception {
+    GerritMaintenance gm =
+        GerritMaintenanceTestHelper.createGerritMaintenanceWithGitGcs(NAMESPACE, List.of(Set.of()));
+    HttpURLConnection http = sendAdmissionRequest(gm);
+
+    AdmissionReview response =
+        new ObjectMapper().readValue(http.getInputStream(), AdmissionReview.class);
+
+    assertThat(http.getResponseCode(), is(equalTo(HttpServletResponse.SC_OK)));
+    assertThat(response.getResponse().getAllowed(), is(true));
+
+    GerritMaintenance gmConflict =
+        GerritMaintenanceTestHelper.createGerritMaintenanceWithGitGcs(
+            NAMESPACE, List.of(Set.of(), Set.of()));
+
+    HttpURLConnection http2 = sendAdmissionRequest(gmConflict);
+
+    AdmissionReview response2 =
+        new ObjectMapper().readValue(http2.getInputStream(), AdmissionReview.class);
+
+    assertThat(http2.getResponseCode(), is(equalTo(HttpServletResponse.SC_OK)));
+    assertThat(response2.getResponse().getAllowed(), is(false));
+    assertThat(
+        response2.getResponse().getStatus().getCode(),
+        is(equalTo(HttpServletResponse.SC_CONFLICT)));
+  }
+
+  @Test
+  @DisplayName(
+      "A GitGc configured to work on all projects and selective GitGcs are allowed to exist at the same time.")
+  public void testSelectiveAndCompleteGitGcAreAllowedTogether() throws Exception {
+    GerritMaintenance gm =
+        GerritMaintenanceTestHelper.createGerritMaintenanceWithGitGcs(
+            NAMESPACE, List.of(Set.of(), Set.of("project1")));
+
+    HttpURLConnection http2 = sendAdmissionRequest(gm);
+
+    AdmissionReview response2 =
+        new ObjectMapper().readValue(http2.getInputStream(), AdmissionReview.class);
+
+    assertThat(http2.getResponseCode(), is(equalTo(HttpServletResponse.SC_OK)));
+    assertThat(response2.getResponse().getAllowed(), is(true));
+  }
+
+  @Test
+  @DisplayName("Multiple selectve GitGcs working on a different set of projects are allowed.")
+  public void testNonConflictingSelectiveGcsAreAllowed() throws Exception {
+    GerritMaintenance gm =
+        GerritMaintenanceTestHelper.createGerritMaintenanceWithGitGcs(
+            NAMESPACE, List.of(Set.of("project1"), Set.of("project2")));
+
+    HttpURLConnection http2 = sendAdmissionRequest(gm);
+
+    AdmissionReview response2 =
+        new ObjectMapper().readValue(http2.getInputStream(), AdmissionReview.class);
+
+    assertThat(http2.getResponseCode(), is(equalTo(HttpServletResponse.SC_OK)));
+    assertThat(response2.getResponse().getAllowed(), is(true));
+  }
+
+  @Test
+  @DisplayName("Multiple selectve GitGcs working on the same project(s) are not allowed.")
+  public void testConflictingSelectiveGcsNotAllowed() throws Exception {
+    GerritMaintenance gm =
+        GerritMaintenanceTestHelper.createGerritMaintenanceWithGitGcs(
+            NAMESPACE, List.of(Set.of("project1"), Set.of("project1")));
+    HttpURLConnection http2 = sendAdmissionRequest(gm);
+
+    AdmissionReview response2 =
+        new ObjectMapper().readValue(http2.getInputStream(), AdmissionReview.class);
+
+    assertThat(http2.getResponseCode(), is(equalTo(HttpServletResponse.SC_OK)));
+    assertThat(response2.getResponse().getAllowed(), is(false));
+    assertThat(
+        response2.getResponse().getStatus().getCode(),
+        is(equalTo(HttpServletResponse.SC_CONFLICT)));
+  }
+
+  private HttpURLConnection sendAdmissionRequest(GerritMaintenance gm)
+      throws MalformedURLException, IOException {
+    HttpURLConnection http =
+        (HttpURLConnection)
+            new URL(
+                    "http://localhost:"
+                        + PORT
+                        + "/admission/"
+                        + Constants.VERSION
+                        + "/"
+                        + Constants.GERRIT_MAINTENANCE_KIND)
+                .openConnection();
+    http.setRequestMethod(HttpMethod.POST.asString());
+    http.setRequestProperty("Content-Type", "application/json");
+    http.setDoOutput(true);
+
+    AdmissionRequest admissionReq = new AdmissionRequest();
+    admissionReq.setObject(gm);
+    AdmissionReview admissionReview = new AdmissionReview();
+    admissionReview.setRequest(admissionReq);
+
+    try (OutputStream os = http.getOutputStream()) {
+      byte[] input = new ObjectMapper().writer().writeValueAsBytes(admissionReview);
+      os.write(input, 0, input.length);
+    }
+    return http;
+  }
+
+  @AfterAll
+  public void shutdown() throws Exception {
+    server.stop();
+  }
+}
diff --git a/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 af5fd54..dd45f78 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
@@ -25,6 +25,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.maintenance.GerritMaintenanceReconciler;
 import com.google.gerrit.k8s.operator.network.GerritNetworkReconcilerProvider;
 import com.google.gerrit.k8s.operator.network.IngressType;
 import com.google.gerrit.k8s.operator.receiver.ReceiverReconciler;
@@ -73,6 +74,7 @@
           .withReconciler(new ReceiverReconciler(client))
           .withReconciler(getGerritNetworkReconciler())
           .withReconciler(new IncomingReplicationTaskReconciler())
+          .withReconciler(new GerritMaintenanceReconciler())
           .build();
 
   @BeforeEach
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/cluster/dependent/gerrit_maintenance.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/cluster/dependent/gerrit_maintenance.yaml
new file mode 100644
index 0000000..ce47b0e
--- /dev/null
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/cluster/dependent/gerrit_maintenance.yaml
@@ -0,0 +1,51 @@
+apiVersion: "gerritoperator.google.com/v1beta14"
+kind: GerritMaintenance
+metadata:
+  name: gerrit-gerrit-maintenance
+  namespace: gerrit
+spec:
+  containerImages: {}
+  storage:
+    storageClasses:
+      readWriteOnce: default
+      readWriteMany: shared-storage
+    sharedStorage:
+      size: 1Gi
+      volumeName: "vol"
+      selector:
+        matchLabels:
+          volume-type: ssd
+          aws-availability-zone: us-east-1
+  projects:
+    gc:
+    - name: gc-all
+      schedule: "0 */3 * * *"
+      args:
+      - "--pack-all-refs"
+      exclude:
+      - "excluded"
+    - name: gc-selected
+      schedule: "0 * * * *"
+      gitOptions: |-
+        [gc]
+          auto = 0
+      include:
+      - "selected"
+      resources:
+        limits:
+          cpu: "100m"
+          memory: "128Mi"
+      affinity:
+        nodeAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+            nodeSelectorTerms:
+            - matchExpressions:
+              - key: "cloud.google.com/gke-nodepool"
+                operator: In
+                values:
+                - "gerrit"
+      tolerations:
+      - key: "key"
+        operator: "Equal"
+        value: "value"
+        effect: "NoSchedule"
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/cluster/dependent/gerrit_primary.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/cluster/dependent/gerrit_primary.yaml
index 56fb64d..b66b9fa 100644
--- a/operator/src/test/resources/com/google/gerrit/k8s/operator/cluster/dependent/gerrit_primary.yaml
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/cluster/dependent/gerrit_primary.yaml
@@ -1,9 +1,9 @@
-apiVersion: gerritoperator.google.com/v1beta13
+apiVersion: gerritoperator.google.com/v1beta14
 kind: Gerrit
 metadata:
   name: gerrit
   annotations:
-    gerritoperator.google.com/apiVersion: gerritoperator.google.com/v1beta13
+    gerritoperator.google.com/apiVersion: gerritoperator.google.com/v1beta14
 spec:
   configFiles: {}
   containerImages:
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/cluster/dependent/gerrit_replica.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/cluster/dependent/gerrit_replica.yaml
index 729bf88..940f34d 100644
--- a/operator/src/test/resources/com/google/gerrit/k8s/operator/cluster/dependent/gerrit_replica.yaml
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/cluster/dependent/gerrit_replica.yaml
@@ -1,9 +1,9 @@
-apiVersion: gerritoperator.google.com/v1beta13
+apiVersion: gerritoperator.google.com/v1beta14
 kind: Gerrit
 metadata:
   name: gerrit-replica
   annotations:
-    gerritoperator.google.com/apiVersion: gerritoperator.google.com/v1beta13
+    gerritoperator.google.com/apiVersion: gerritoperator.google.com/v1beta14
 spec:
   configFiles: {}
   containerImages:
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/cluster/gerritcluster_primary_replica.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/cluster/gerritcluster_primary_replica.yaml
index b1b0be7..5faa94b 100644
--- a/operator/src/test/resources/com/google/gerrit/k8s/operator/cluster/gerritcluster_primary_replica.yaml
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/cluster/gerritcluster_primary_replica.yaml
@@ -1,7 +1,8 @@
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: GerritCluster
 metadata:
   name: gerrit
+  namespace: gerrit
 spec:
   containerImages: {}
 
@@ -67,3 +68,39 @@
       libs: []
       configFiles: {}
       secretRef: gerrit-secure-config
+
+  scheduledTasks:
+    gerritMaintenance:
+      projects:
+        gc:
+        - name: gc-all
+          schedule: "0 */3 * * *"
+          args:
+          - "--pack-all-refs"
+          exclude:
+          - "excluded"
+        - name: gc-selected
+          schedule: "0 * * * *"
+          gitOptions: |-
+            [gc]
+              auto = 0
+          include:
+          - "selected"
+          resources:
+            limits:
+              cpu: "100m"
+              memory: "128Mi"
+          affinity:
+            nodeAffinity:
+              requiredDuringSchedulingIgnoredDuringExecution:
+                nodeSelectorTerms:
+                - matchExpressions:
+                  - key: "cloud.google.com/gke-nodepool"
+                    operator: In
+                    values:
+                    - "gerrit"
+          tolerations:
+          - key: "key"
+            operator: "Equal"
+            value: "value"
+            effect: "NoSchedule"
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/gerrit/gerrit_es_primary.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/gerrit/gerrit_es_primary.yaml
index cf9a101..dd510d1 100644
--- a/operator/src/test/resources/com/google/gerrit/k8s/operator/gerrit/gerrit_es_primary.yaml
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/gerrit/gerrit_es_primary.yaml
@@ -1,4 +1,4 @@
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: Gerrit
 metadata:
   name: gerrit
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/gerrit/gerrit_ha_primary.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/gerrit/gerrit_ha_primary.yaml
index 7d97b59..4116b0a 100644
--- a/operator/src/test/resources/com/google/gerrit/k8s/operator/gerrit/gerrit_ha_primary.yaml
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/gerrit/gerrit_ha_primary.yaml
@@ -1,4 +1,4 @@
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: Gerrit
 metadata:
   name: gerrit
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/gerrit/gerrit_single_primary.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/gerrit/gerrit_single_primary.yaml
index 878510d..dd2e5b1 100644
--- a/operator/src/test/resources/com/google/gerrit/k8s/operator/gerrit/gerrit_single_primary.yaml
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/gerrit/gerrit_single_primary.yaml
@@ -1,4 +1,4 @@
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: Gerrit
 metadata:
   name: gerrit
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/dependent/cronjob_all_default.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/dependent/cronjob_all_default.yaml
index 2b1a40b..f3fac82 100644
--- a/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/dependent/cronjob_all_default.yaml
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/dependent/cronjob_all_default.yaml
@@ -14,7 +14,7 @@
   annotations:
     app.kubernetes.io/managed-by: gerrit-operator
   ownerReferences:
-  - apiVersion: gerritoperator.google.com/v1beta13
+  - apiVersion: gerritoperator.google.com/v1beta14
     kind: GitGarbageCollection
     name: gitgc
     uid: abcd1234
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/dependent/cronjob_all_nfs_workaround.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/dependent/cronjob_all_nfs_workaround.yaml
index df97731..82b6a8d 100644
--- a/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/dependent/cronjob_all_nfs_workaround.yaml
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/dependent/cronjob_all_nfs_workaround.yaml
@@ -14,7 +14,7 @@
   annotations:
     app.kubernetes.io/managed-by: gerrit-operator
   ownerReferences:
-  - apiVersion: gerritoperator.google.com/v1beta13
+  - apiVersion: gerritoperator.google.com/v1beta14
     kind: GitGarbageCollection
     name: gitgc
     uid: abcd1234
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/dependent/cronjob_all_options_enabled.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/dependent/cronjob_all_options_enabled.yaml
index 9bc45e5..337d3bf 100644
--- a/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/dependent/cronjob_all_options_enabled.yaml
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/dependent/cronjob_all_options_enabled.yaml
@@ -14,7 +14,7 @@
   annotations:
     app.kubernetes.io/managed-by: gerrit-operator
   ownerReferences:
-  - apiVersion: gerritoperator.google.com/v1beta13
+  - apiVersion: gerritoperator.google.com/v1beta14
     kind: GitGarbageCollection
     name: gitgc
     uid: abcd1234
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/dependent/cronjob_selected_default.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/dependent/cronjob_selected_default.yaml
index 71d3651..f95e381 100644
--- a/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/dependent/cronjob_selected_default.yaml
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/dependent/cronjob_selected_default.yaml
@@ -14,7 +14,7 @@
   annotations:
     app.kubernetes.io/managed-by: gerrit-operator
   ownerReferences:
-  - apiVersion: gerritoperator.google.com/v1beta13
+  - apiVersion: gerritoperator.google.com/v1beta14
     kind: GitGarbageCollection
     name: gitgc
     uid: abcd1234
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/dependent/cronjob_selected_options_enabled.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/dependent/cronjob_selected_options_enabled.yaml
index ecd3742..bc67902 100644
--- a/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/dependent/cronjob_selected_options_enabled.yaml
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/dependent/cronjob_selected_options_enabled.yaml
@@ -14,7 +14,7 @@
   annotations:
     app.kubernetes.io/managed-by: gerrit-operator
   ownerReferences:
-  - apiVersion: gerritoperator.google.com/v1beta13
+  - apiVersion: gerritoperator.google.com/v1beta14
     kind: GitGarbageCollection
     name: gitgc
     uid: abcd1234
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/gerritcluster_minimal.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/gerritcluster_minimal.yaml
index dac2028..9b9824f 100644
--- a/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/gerritcluster_minimal.yaml
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/gerritcluster_minimal.yaml
@@ -1,4 +1,4 @@
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: GerritCluster
 metadata:
   name: gerrit
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/gerritcluster_nfs_workaround.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/gerritcluster_nfs_workaround.yaml
index a73be87..4585397 100644
--- a/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/gerritcluster_nfs_workaround.yaml
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/gerritcluster_nfs_workaround.yaml
@@ -1,4 +1,4 @@
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: GerritCluster
 metadata:
   name: gerrit
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/gitgc_all_default.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/gitgc_all_default.yaml
index 27c329f..c99da00 100644
--- a/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/gitgc_all_default.yaml
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/gitgc_all_default.yaml
@@ -1,4 +1,4 @@
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: GitGarbageCollection
 metadata:
   name: gitgc
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/gitgc_all_options_enabled.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/gitgc_all_options_enabled.yaml
index 588695f..28a3795 100644
--- a/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/gitgc_all_options_enabled.yaml
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/gitgc_all_options_enabled.yaml
@@ -1,4 +1,4 @@
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: GitGarbageCollection
 metadata:
   name: gitgc
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/gitgc_selected_default.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/gitgc_selected_default.yaml
index 63b3e5f..85c1fc1 100644
--- a/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/gitgc_selected_default.yaml
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/gitgc_selected_default.yaml
@@ -1,4 +1,4 @@
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: GitGarbageCollection
 metadata:
   name: gitgc
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/gitgc_selected_options_enabled.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/gitgc_selected_options_enabled.yaml
index 904db1c..2c94d05 100644
--- a/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/gitgc_selected_options_enabled.yaml
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/gitgc/gitgc_selected_options_enabled.yaml
@@ -1,4 +1,4 @@
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: GitGarbageCollection
 metadata:
   name: gitgc
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/indexer/gerritcluster_es.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/indexer/gerritcluster_es.yaml
index 48e8108..788792c 100644
--- a/operator/src/test/resources/com/google/gerrit/k8s/operator/indexer/gerritcluster_es.yaml
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/indexer/gerritcluster_es.yaml
@@ -1,4 +1,4 @@
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: GerritCluster
 metadata:
   name: gerrit
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/indexer/gerritcluster_minimal.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/indexer/gerritcluster_minimal.yaml
index 164a8d9..7f850e8 100644
--- a/operator/src/test/resources/com/google/gerrit/k8s/operator/indexer/gerritcluster_minimal.yaml
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/indexer/gerritcluster_minimal.yaml
@@ -1,4 +1,4 @@
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: GerritCluster
 metadata:
   name: gerrit
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/indexer/indexer.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/indexer/indexer.yaml
index 7e6174b..6cb5abc 100644
--- a/operator/src/test/resources/com/google/gerrit/k8s/operator/indexer/indexer.yaml
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/indexer/indexer.yaml
@@ -1,4 +1,4 @@
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: GerritIndexer
 metadata:
   name: gerrit-indexer
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/indexer/indexer_es.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/indexer/indexer_es.yaml
index 8da67ec..1ff7c38 100644
--- a/operator/src/test/resources/com/google/gerrit/k8s/operator/indexer/indexer_es.yaml
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/indexer/indexer_es.yaml
@@ -1,4 +1,4 @@
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: GerritIndexer
 metadata:
   name: gerrit-indexer
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/maintenance/dependent/cronjob_gitgc_all.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/maintenance/dependent/cronjob_gitgc_all.yaml
new file mode 100644
index 0000000..82a0946
--- /dev/null
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/maintenance/dependent/cronjob_gitgc_all.yaml
@@ -0,0 +1,73 @@
+apiVersion: batch/v1
+kind: CronJob
+metadata:
+  name: gc-all
+  namespace: gerrit
+  labels:
+    app.kubernetes.io/managed-by: gerrit-operator
+    app.kubernetes.io/name: gerrit
+    app.kubernetes.io/part-of: git-gc-gc-all
+    app.kubernetes.io/created-by: GerritMaintenanceReconciler
+    app.kubernetes.io/instance: git-gc-gc-all
+    app.kubernetes.io/version: unknown
+    app.kubernetes.io/component: gerrit-maintenance-gerrit-gerrit-maintenance
+spec:
+  schedule: "0 */3 * * *"
+  concurrencyPolicy: Forbid
+  jobTemplate:
+    spec:
+      template:
+        metadata:
+          annotations:
+            cluster-autoscaler.kubernetes.io/safe-to-evict: false
+            sidecar.istio.io/inject: false
+          labels:
+            app.kubernetes.io/managed-by: gerrit-operator
+            app.kubernetes.io/name: gerrit
+            app.kubernetes.io/part-of: git-gc-gc-all
+            app.kubernetes.io/created-by: GerritMaintenanceReconciler
+            app.kubernetes.io/instance: git-gc-gc-all
+            app.kubernetes.io/version: unknown
+            app.kubernetes.io/component: gerrit-maintenance-gerrit-gerrit-maintenance
+        spec:
+          restartPolicy: OnFailure
+          securityContext:
+            fsGroup: 100
+            runAsGroup: 100
+            runAsUser: 1000
+            runAsNonRoot: true
+            seccompProfile:
+              type: RuntimeDefault
+          containers:
+          - name: git-gc
+            securityContext:
+              runAsGroup: 100
+              runAsUser: 1000
+              runAsNonRoot: true
+              readOnlyRootFilesystem: true
+              allowPrivilegeEscalation: false
+              seccompProfile:
+                type: RuntimeDefault
+              capabilities:
+                drop:
+                - ALL
+            imagePullPolicy: Always
+            image: docker.io/k8sgerrit/gerrit-maintenance:latest
+            args:
+            - "-d"
+            - "/var/gerrit"
+            - "projects"
+            - "--skip"
+            - "excluded"
+            - "--skip"
+            - "selected"
+            - "gc"
+            - "--pack-all-refs"
+            volumeMounts:
+            - name: shared
+              subPath: git
+              mountPath: /var/gerrit/git
+          volumes:
+          - name: shared
+            persistentVolumeClaim:
+              claimName: shared-pvc
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/maintenance/dependent/cronjob_gitgc_selected.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/maintenance/dependent/cronjob_gitgc_selected.yaml
new file mode 100644
index 0000000..1a3e633
--- /dev/null
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/maintenance/dependent/cronjob_gitgc_selected.yaml
@@ -0,0 +1,89 @@
+apiVersion: batch/v1
+kind: CronJob
+metadata:
+  name: gc-selected
+  namespace: gerrit
+  labels:
+    app.kubernetes.io/managed-by: gerrit-operator
+    app.kubernetes.io/name: gerrit
+    app.kubernetes.io/part-of: git-gc-gc-selected
+    app.kubernetes.io/created-by: GerritMaintenanceReconciler
+    app.kubernetes.io/instance: git-gc-gc-selected
+    app.kubernetes.io/version: unknown
+    app.kubernetes.io/component: gerrit-maintenance-gerrit-gerrit-maintenance
+spec:
+  schedule: "0 * * * *"
+  concurrencyPolicy: Forbid
+  jobTemplate:
+    spec:
+      template:
+        metadata:
+          annotations:
+            cluster-autoscaler.kubernetes.io/safe-to-evict: false
+            sidecar.istio.io/inject: false
+          labels:
+            app.kubernetes.io/managed-by: gerrit-operator
+            app.kubernetes.io/name: gerrit
+            app.kubernetes.io/part-of: git-gc-gc-selected
+            app.kubernetes.io/created-by: GerritMaintenanceReconciler
+            app.kubernetes.io/instance: git-gc-gc-selected
+            app.kubernetes.io/version: unknown
+            app.kubernetes.io/component: gerrit-maintenance-gerrit-gerrit-maintenance
+        spec:
+          restartPolicy: OnFailure
+          securityContext:
+            fsGroup: 100
+            runAsGroup: 100
+            runAsUser: 1000
+            runAsNonRoot: true
+            seccompProfile:
+              type: RuntimeDefault
+          affinity:
+            nodeAffinity:
+              requiredDuringSchedulingIgnoredDuringExecution:
+                nodeSelectorTerms:
+                - matchExpressions:
+                  - key: "cloud.google.com/gke-nodepool"
+                    operator: In
+                    values:
+                    - "gerrit"
+          tolerations:
+          - key: "key"
+            operator: "Equal"
+            value: "value"
+            effect: "NoSchedule"
+          containers:
+          - name: git-gc
+            securityContext:
+              runAsGroup: 100
+              runAsUser: 1000
+              runAsNonRoot: true
+              readOnlyRootFilesystem: true
+              allowPrivilegeEscalation: false
+              seccompProfile:
+                type: RuntimeDefault
+              capabilities:
+                drop:
+                - ALL
+            imagePullPolicy: Always
+            image: docker.io/k8sgerrit/gerrit-maintenance:latest
+            args:
+            - "-d"
+            - "/var/gerrit"
+            - "projects"
+            - "--project"
+            - "selected"
+            - "gc"
+            - "-c gc.auto=0"
+            resources:
+              limits:
+                cpu: "100m"
+                memory: "128Mi"
+            volumeMounts:
+            - name: shared
+              subPath: git
+              mountPath: /var/gerrit/git
+          volumes:
+          - name: shared
+            persistentVolumeClaim:
+              claimName: shared-pvc
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/maintenance/gerrit-maintenance.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/maintenance/gerrit-maintenance.yaml
new file mode 100644
index 0000000..ce47b0e
--- /dev/null
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/maintenance/gerrit-maintenance.yaml
@@ -0,0 +1,51 @@
+apiVersion: "gerritoperator.google.com/v1beta14"
+kind: GerritMaintenance
+metadata:
+  name: gerrit-gerrit-maintenance
+  namespace: gerrit
+spec:
+  containerImages: {}
+  storage:
+    storageClasses:
+      readWriteOnce: default
+      readWriteMany: shared-storage
+    sharedStorage:
+      size: 1Gi
+      volumeName: "vol"
+      selector:
+        matchLabels:
+          volume-type: ssd
+          aws-availability-zone: us-east-1
+  projects:
+    gc:
+    - name: gc-all
+      schedule: "0 */3 * * *"
+      args:
+      - "--pack-all-refs"
+      exclude:
+      - "excluded"
+    - name: gc-selected
+      schedule: "0 * * * *"
+      gitOptions: |-
+        [gc]
+          auto = 0
+      include:
+      - "selected"
+      resources:
+        limits:
+          cpu: "100m"
+          memory: "128Mi"
+      affinity:
+        nodeAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+            nodeSelectorTerms:
+            - matchExpressions:
+              - key: "cloud.google.com/gke-nodepool"
+                operator: In
+                values:
+                - "gerrit"
+      tolerations:
+      - key: "key"
+        operator: "Equal"
+        value: "value"
+        effect: "NoSchedule"
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
index 9fde8a3..a0031e9 100644
--- 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
@@ -1,4 +1,4 @@
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: GerritNetwork
 metadata:
   name: gerrit
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
index a3a7ec8..633d9d8 100644
--- 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
@@ -1,4 +1,4 @@
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: GerritNetwork
 metadata:
   name: gerrit
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
index aaa3e26..02aeaac 100644
--- 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
@@ -1,4 +1,4 @@
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: GerritNetwork
 metadata:
   name: gerrit
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
index b1bfa0e..78693ff 100644
--- 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
@@ -1,4 +1,4 @@
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: GerritNetwork
 metadata:
   name: gerrit
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
index 8e620e3..1df7318 100644
--- 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
@@ -1,4 +1,4 @@
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: GerritNetwork
 metadata:
   name: gerrit
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
index 0d72ec1..fe04e69 100644
--- 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
@@ -1,4 +1,4 @@
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: GerritNetwork
 metadata:
   name: gerrit
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
index 58f290f..3cd5afa 100644
--- 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
@@ -1,4 +1,4 @@
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: GerritNetwork
 metadata:
   name: gerrit
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
index 170c94f..21efe47 100644
--- 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
@@ -1,4 +1,4 @@
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: GerritNetwork
 metadata:
   name: gerrit
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
index cfd215f..73361e6 100644
--- 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
@@ -1,4 +1,4 @@
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: GerritNetwork
 metadata:
   name: gerrit
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
index 87837a5..77b6283 100644
--- 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
@@ -1,4 +1,4 @@
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: GerritNetwork
 metadata:
   name: gerrit
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
index 4281047..6e754f3 100644
--- 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
@@ -1,4 +1,4 @@
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: Receiver
 metadata:
   name: receiver
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
index ee6ebd6..b49f16e 100644
--- 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
@@ -1,4 +1,4 @@
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: Receiver
 metadata:
   name: receiver
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/tasks/incomingrepl/gerritcluster_incomingrepl.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/tasks/incomingrepl/gerritcluster_incomingrepl.yaml
index 00b1ecd..118ad11 100644
--- a/operator/src/test/resources/com/google/gerrit/k8s/operator/tasks/incomingrepl/gerritcluster_incomingrepl.yaml
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/tasks/incomingrepl/gerritcluster_incomingrepl.yaml
@@ -1,4 +1,4 @@
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: GerritCluster
 metadata:
   name: gerrit
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/tasks/incomingrepl/incomingrepltask.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/tasks/incomingrepl/incomingrepltask.yaml
index 31e6a3e..f5e85fc 100644
--- a/operator/src/test/resources/com/google/gerrit/k8s/operator/tasks/incomingrepl/incomingrepltask.yaml
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/tasks/incomingrepl/incomingrepltask.yaml
@@ -1,4 +1,4 @@
-apiVersion: "gerritoperator.google.com/v1beta13"
+apiVersion: "gerritoperator.google.com/v1beta14"
 kind: IncomingReplicationTask
 metadata:
   name: incoming-replication-test