[Operator] Add support for Ambassador ingress type

This change adds an Ambassador-based GerritNetworkReconciler, which is
used when the `INGRESS` environment variable for the operator is set to
"ambassador". In this case, the precondition is that the Ambassador CRDs
must already be pre-deployed in the k8s cluster. This change uses the
CRD version `getambassador.io/v2`.

Ambassador (also known as "Emissary") is an open-source ingress provider
that sets up ingresses for services via Custom Resources such as
`Mapping`, `TLSContext`, etc. The newly created
GerritAmbassadorReconciler creates and manages these resources as josdk
"dependent resources". The mappings are created to direct traffic to
Primary Gerrit and/or Replica Gerrit and/or Receivers in the
GerritCluster. If a GerritCluster has both a Primary and a Replica, then
all read traffic (git fetch/clone requests) is directed to the Replica,
and all write traffic (git push) is directed to the Primary.

Because there does not exist a fabric8 extension for Ambassador custom
resources, managing Ambassador CRs in the operator is a little tricky.
Two options were considered:
1. Use fabric8 k8s client's `GenericKubernetesResource` class.
`GenericKubernetesResource` implements the `HasMetadata` interface, just
like the `CustomResource` parent class that is used to define custom
resources like GerritCluster etc. `GenericKubernetesResource` can be
used to create custom resources by defining the resource spec as a Java
Map<String, String>. However, with this option, we would need to
subclass `GenericKubernetesResource` (e.g. `OperatorMappingWrapper`) to
be able to provide an apiVersion and/or group (expected by josdk). This
would introduce an unnecessary CRD to the operator, which is not
desirable.
2. Generate Ambassador custom resource POJOs from the CRD yaml using
`java-generator-maven-plugin`. This makes it such that it appears to the
operator that the Ambassador resources were manually defined in the
source code as Java classes, just like the other resources -
GerritCluster, Gerrit, etc.

We went with option 2.

Ambassador CRDs are fetched from
https://github.com/emissary-ingress/emissary/blob/master/manifests/emissary/emissary-crds.yaml.in
and stored in the repo as a yaml file. The `java-generator-maven-plugin`
(fabric8 project) is used to generate POJOs from this CRD yaml. The
POJOs represent the Ambassador CRs in Java classes.

Manual edits to the Ambassador CRDs
- The yaml file defines many CRDs but we only need `Mapping` and
`TLSContext` for this change so the rest of the CRDs are deleted from
the file
- The generator plugin has a bug while converting enum types
(https://github.com/fabric8io/kubernetes-client/issues/5457). To avoid
hitting this bug, the `v2ExplicitTLS` field in the Mapping CRD
`v3alpha1` version is commented out
- Emissary CRD apiVersions are not self-contained. There is a field
`ambassador_id` in `Mapping` and `TLSContext` CRD that is not defined in
apiVersion v2 but users are still able to create v2 Mappings with
ambassador_id field (Emissary converts v2 resources to v3 via webhooks
defined in emissary service). To be able to define `ambassador_id` in
the Mapping/TLSContext CRs created via the operator, we manually add
`ambassador_id` to the v2 Mapping and TLSContext CRD.

Currently, the operator is watching for changes in resources in all
namespaces. If you have Ambassador resources deployed in your k8s
cluster that use both v1 and v2 apiVersions, you might run into
deserialization errors as the operator attempts to deserialize the
existing v1 resources into the v2 POJOs.

Change-Id: I23d446e21da87e33c71fe3dad481ec34cd963bbe
diff --git a/.gitignore b/.gitignore
index 8a09c99..ea7768a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,7 @@
 __pycache__
 .pytest_cache
 *.pyc
+
+*bin/
+.DS_Store
+.vscode/
diff --git a/Documentation/operator.md b/Documentation/operator.md
index 55eed96..919e217 100644
--- a/Documentation/operator.md
+++ b/Documentation/operator.md
@@ -157,9 +157,14 @@
 will be routed to the intended GerritCluster component, e.g. in case a primary
 Gerrit and a Gerrit Replica exist in the cluster, git fetch/clone requests will
 be sent to the Gerrit Replica and all other requests to the primary Gerrit.
-The Gerrit Operator currently supports the following Ingress providers, which
-can be configured for each
-[GerritCluster](operator-api-reference.md#gerritclusteringressconfig):
+
+You may specify the ingress provider by setting the `INGRESS` environment
+variable in the operator Deployment manifest. That is, the choice of an ingress
+provider is an operator-level setting. However, you may specify some ingress
+configuration options (host, tls, etc) at the `GerritCluster` level, via
+[GerritClusterIngressConfig](operator-api-reference.md#gerritclusteringressconfig).
+
+The Gerrit Operator currently supports the following Ingress providers:
 
 - **NONE**
 
@@ -186,6 +191,15 @@
   The operator supports the use of [Istio](https://istio.io/) as a service mesh.
   An example on how to set up Istio can be found [here](../istio/gerrit.profile.yaml).
 
+- **AMBASSADOR**
+
+  The operator also supports [Ambassador](https://www.getambassador.io/) for
+  setting up ingress to the Gerrits deployed by the operator. If you use
+  Ambassador's "Edge Stack" or "Emissary Ingress" to provide ingress to your k8s
+  services, you should set INGRESS=AMBASSADOR. Currently, SSH is not directly
+  supported when using INGRESS=AMBASSADOR.
+
+
 ## Deploy
 You will need to have admin privileges for your k8s cluster in order to be able
 to deploy the following resources.
@@ -280,7 +294,8 @@
 `k8s/operator.yaml` contains a basic deployment of the operator. Resources,
 docker image name etc. might have to be adapted. For example, the ingress
 provider has to be configured by setting the `INGRESS` environment variable
-in `operator/k8s/operator.yaml` to either `NONE`, `INGRESS` or `ISTIO`.
+in `operator/k8s/operator.yaml` to either `NONE`, `INGRESS`, `ISTIO`, or
+`AMBASSADOR`.
 
 ## CustomResources
 
diff --git a/LICENSE b/LICENSE
index 5e173a3..27bdfb6 100644
--- a/LICENSE
+++ b/LICENSE
@@ -255,6 +255,11 @@
 Copyright 2022 Software Freedom Conservancy (SFC) \
 Apache 2 license (https://github.com/SeleniumHQ/selenium/blob/trunk/LICENSE)
 
+Ambassador \
+https://github.com/emissary-ingress/emissary \
+Copyright 2021 Ambassador Labs \
+Apache 2 license (https://github.com/emissary-ingress/emissary/blob/master/LICENSE)
+
 ---
 ## The MIT License (MIT)
 
diff --git a/operator/pom.xml b/operator/pom.xml
index ce7a44c..3697fbc 100644
--- a/operator/pom.xml
+++ b/operator/pom.xml
@@ -19,6 +19,7 @@
 		<flogger.version>0.7.4</flogger.version>
 		<javaoperatorsdk.version>4.3.3</javaoperatorsdk.version>
 		<jetty.version>11.0.15</jetty.version>
+		<lombok.version>1.18.28</lombok.version>
 		<maven.compiler.source>11</maven.compiler.source>
 		<maven.compiler.target>11</maven.compiler.target>
 		<docker.registry>docker.io</docker.registry>
@@ -172,6 +173,17 @@
 			<scope>provided</scope>
 		</dependency>
 		<dependency>
+			<groupId>io.fabric8</groupId>
+			<artifactId>generator-annotations</artifactId>
+			<version>${fabric8.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.projectlombok</groupId>
+			<artifactId>lombok</artifactId>
+			<scope>provided</scope>
+			<version>${lombok.version}</version>
+		</dependency>
+		<dependency>
 			<groupId>org.eclipse.jetty</groupId>
 			<artifactId>jetty-server</artifactId>
 			<version>${jetty.version}</version>
@@ -245,6 +257,23 @@
 	<build>
 		<plugins>
 			<plugin>
+				<groupId>io.fabric8</groupId>
+				<artifactId>java-generator-maven-plugin</artifactId>
+				<version>${fabric8.version}</version>
+				<configuration>
+					<source>${project.basedir}/src/main/resources/crd/emissary-crds.yaml</source>
+					<!-- Generate sundrio @Buildable annotations that generate Builder classes-->
+					<extraAnnotations>true</extraAnnotations>
+				</configuration>
+				<executions>
+					<execution>
+						<goals>
+							<goal>generate</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+			<plugin>
 				<groupId>com.spotify.fmt</groupId>
 				<artifactId>fmt-maven-plugin</artifactId>
 				<version>2.19</version>
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/GerritTemplate.java b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/GerritTemplate.java
index 4c1fa12..602431a 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/GerritTemplate.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/model/GerritTemplate.java
@@ -33,7 +33,7 @@
 @Buildable(
     editableEnabled = false,
     validationEnabled = false,
-    generateBuilderPackage = true,
+    generateBuilderPackage = false,
     lazyCollectionInitEnabled = false,
     builderPackage = "io.fabric8.kubernetes.api.builder")
 public class GerritTemplate implements KubernetesResource {
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/network/Constants.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/Constants.java
new file mode 100644
index 0000000..1a7f3b2
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/network/Constants.java
@@ -0,0 +1,22 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.package com.google.gerrit.k8s.operator.network;
+
+package com.google.gerrit.k8s.operator.network;
+
+public class Constants {
+  public static String UPLOAD_PACK_URL_PATTERN = "/.*/git-upload-pack";
+  public static String INFO_REFS_PATTERN = "/.*/info/refs";
+  public static String RECEIVE_PACK_URL_PATTERN = "/.*/git-receive-pack";
+  public static String PROJECTS_URL_PATTERN = "/a/projects/.*";
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/network/GerritNetworkReconcilerProvider.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/GerritNetworkReconcilerProvider.java
index 5390bd3..5d68e81 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/network/GerritNetworkReconcilerProvider.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/network/GerritNetworkReconcilerProvider.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.k8s.operator.network;
 
+import com.google.gerrit.k8s.operator.network.ambassador.GerritAmbassadorReconciler;
 import com.google.gerrit.k8s.operator.network.ingress.GerritIngressReconciler;
 import com.google.gerrit.k8s.operator.network.istio.GerritIstioReconciler;
 import com.google.gerrit.k8s.operator.network.model.GerritNetwork;
@@ -38,6 +39,8 @@
         return new GerritIngressReconciler();
       case ISTIO:
         return new GerritIstioReconciler();
+      case AMBASSADOR:
+        return new GerritAmbassadorReconciler();
       default:
         return new GerritNoIngressReconciler();
     }
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/network/IngressType.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/IngressType.java
index a4af09e..9c5383c 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/network/IngressType.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/network/IngressType.java
@@ -17,5 +17,6 @@
 public enum IngressType {
   NONE,
   INGRESS,
-  ISTIO
+  ISTIO,
+  AMBASSADOR
 }
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/GerritAmbassadorReconciler.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/GerritAmbassadorReconciler.java
new file mode 100644
index 0000000..2b9f594
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/GerritAmbassadorReconciler.java
@@ -0,0 +1,156 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador;
+
+import static com.google.gerrit.k8s.operator.network.ambassador.GerritAmbassadorReconciler.MAPPING_EVENT_SOURCE;
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMapping.GERRIT_MAPPING;
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingGETReplica.GERRIT_MAPPING_GET_REPLICA;
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingPOSTReplica.GERRIT_MAPPING_POST_REPLICA;
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingPrimary.GERRIT_MAPPING_PRIMARY;
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingReceiver.GERRIT_MAPPING_RECEIVER;
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingReceiverGET.GERRIT_MAPPING_RECEIVER_GET;
+
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMapping;
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingGETReplica;
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingPOSTReplica;
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingPrimary;
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingReceiver;
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingReceiverGET;
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.LoadBalanceCondition;
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.ReceiverMappingCondition;
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.SingleMappingCondition;
+import com.google.gerrit.k8s.operator.network.model.GerritNetwork;
+import com.google.inject.Singleton;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
+import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer;
+import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
+import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
+import io.javaoperatorsdk.operator.processing.event.source.EventSource;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Provides an Ambassador-based implementation for GerritNetworkReconciler.
+ *
+ * <p>Creates and manages Ambassador Custom Resources using the "managed dependent resources"
+ * approach in josdk. Since multiple dependent resources of the same type (`Mapping`) need to be
+ * created, "resource discriminators" are used for each of the different Mapping dependent
+ * resources.
+ *
+ * <p>Ambassador custom resource POJOs are generated via the `java-generator-maven-plugin` in the
+ * fabric8 project.
+ *
+ * <p>Mapping logic
+ *
+ * <p>The Mappings are created based on the composition of Gerrit instances in the GerritCluster.
+ *
+ * <p>There are three cases:
+ *
+ * <p>1. 0 Primary 1 Replica
+ *
+ * <p>Direct all traffic (read/write) to the Replica
+ *
+ * <p>2. 1 Primary 0 Replica
+ *
+ * <p>Direct all traffic (read/write) to the Primary
+ *
+ * <p>3. 1 Primary 1 Replica
+ *
+ * <p>Direct write traffic to Primary and read traffic to Replica. To capture this requirement,
+ * three different Mappings have to be created.
+ *
+ * <p>Note: git fetch/clone operations result in two HTTP requests to the git server. The first is
+ * of the form `GET /my-test-repo/info/refs?service=git-upload-pack` and the second is of the form
+ * `POST /my-test-repo/git-upload-pack`.
+ *
+ * <p>Note: git push operations result in two HTTP requests to the git server. The first is of the
+ * form `GET /my-test-repo/info/refs?service=git-receive-pack` and the second is of the form `POST
+ * /my-test-repo/git-receive-pack`.
+ *
+ * <p>If a Receiver is part of the GerritCluster, additional mappings are created such that all
+ * requests that the replication plugin sends to the `adminUrl` [1] are routed to the Receiver. This
+ * includes `git push` related `GET` and `POST` requests, and requests to the `/projects` REST API
+ * endpoints.
+ *
+ * <p>[1]
+ * https://gerrit.googlesource.com/plugins/replication/+/refs/heads/master/src/main/resources/Documentation/config.md
+ */
+@Singleton
+@ControllerConfiguration(
+    // namespaces = "gerrit-operator",
+    dependents = {
+      @Dependent(
+          name = GERRIT_MAPPING,
+          type = GerritClusterMapping.class,
+          // Cluster has only either Primary or Replica instance
+          reconcilePrecondition = SingleMappingCondition.class,
+          useEventSourceWithName = MAPPING_EVENT_SOURCE),
+      @Dependent(
+          name = GERRIT_MAPPING_POST_REPLICA,
+          type = GerritClusterMappingPOSTReplica.class,
+          // Cluster has both Primary and Replica instances
+          reconcilePrecondition = LoadBalanceCondition.class,
+          useEventSourceWithName = MAPPING_EVENT_SOURCE),
+      @Dependent(
+          name = GERRIT_MAPPING_GET_REPLICA,
+          type = GerritClusterMappingGETReplica.class,
+          reconcilePrecondition = LoadBalanceCondition.class,
+          useEventSourceWithName = MAPPING_EVENT_SOURCE),
+      @Dependent(
+          name = GERRIT_MAPPING_PRIMARY,
+          type = GerritClusterMappingPrimary.class,
+          reconcilePrecondition = LoadBalanceCondition.class,
+          useEventSourceWithName = MAPPING_EVENT_SOURCE),
+      @Dependent(
+          name = GERRIT_MAPPING_RECEIVER,
+          type = GerritClusterMappingReceiver.class,
+          reconcilePrecondition = ReceiverMappingCondition.class,
+          useEventSourceWithName = MAPPING_EVENT_SOURCE),
+      @Dependent(
+          name = GERRIT_MAPPING_RECEIVER_GET,
+          type = GerritClusterMappingReceiverGET.class,
+          reconcilePrecondition = ReceiverMappingCondition.class,
+          useEventSourceWithName = MAPPING_EVENT_SOURCE)
+    })
+public class GerritAmbassadorReconciler
+    implements Reconciler<GerritNetwork>, EventSourceInitializer<GerritNetwork> {
+
+  public static final String MAPPING_EVENT_SOURCE = "mapping-event-source";
+
+  // Because we have multiple dependent resources of the same type `Mapping`, we need to specify
+  // a named event source.
+  @Override
+  public Map<String, EventSource> prepareEventSources(EventSourceContext<GerritNetwork> context) {
+    InformerEventSource<Mapping, GerritNetwork> mappingEventSource =
+        new InformerEventSource<>(
+            InformerConfiguration.from(Mapping.class, context).build(), context);
+
+    Map<String, EventSource> eventSources = new HashMap<>();
+    eventSources.put(MAPPING_EVENT_SOURCE, mappingEventSource);
+    return eventSources;
+  }
+
+  @Override
+  public UpdateControl<GerritNetwork> reconcile(
+      GerritNetwork resource, Context<GerritNetwork> context) throws Exception {
+    return UpdateControl.noUpdate();
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/AbstractAmbassadorDependentResource.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/AbstractAmbassadorDependentResource.java
new file mode 100644
index 0000000..b6438ee
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/AbstractAmbassadorDependentResource.java
@@ -0,0 +1,62 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import com.google.gerrit.k8s.operator.cluster.model.GerritCluster;
+import com.google.gerrit.k8s.operator.network.model.GerritNetwork;
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.fabric8.kubernetes.api.model.ObjectMeta;
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+import io.getambassador.v2.MappingSpec;
+import io.getambassador.v2.MappingSpecBuilder;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
+import java.util.List;
+
+public abstract class AbstractAmbassadorDependentResource<T extends HasMetadata>
+    extends CRUDKubernetesDependentResource<T, GerritNetwork> {
+
+  public AbstractAmbassadorDependentResource(Class<T> dependentResourceClass) {
+    super(dependentResourceClass);
+  }
+
+  public ObjectMeta getCommonMetadata(GerritNetwork gerritnetwork, String name, String className) {
+    ObjectMeta metadata =
+        new ObjectMetaBuilder()
+            .withName(name)
+            .withNamespace(gerritnetwork.getMetadata().getNamespace())
+            .withLabels(
+                GerritCluster.getLabels(gerritnetwork.getMetadata().getName(), name, className))
+            .build();
+    return metadata;
+  }
+
+  public MappingSpec getCommonSpec(GerritNetwork gerritnetwork, String serviceName) {
+    MappingSpec spec =
+        new MappingSpecBuilder()
+            .withAmbassadorId(getAmbassadorIds(gerritnetwork))
+            .withHost(gerritnetwork.getSpec().getIngress().getHost())
+            .withPrefix("/")
+            .withService(serviceName)
+            .withBypassAuth(true)
+            .withRewrite("") // important - so the prefix doesn't get overwritten to "/"
+            .build();
+    return spec;
+  }
+
+  public List<String> getAmbassadorIds(GerritNetwork gerritnetwork) {
+    // TODO: Allow users to configure ambassador_id
+    return null;
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMapping.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMapping.java
new file mode 100644
index 0000000..e6f63ed
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMapping.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import com.google.gerrit.k8s.operator.network.model.GerritNetwork;
+import com.google.gerrit.k8s.operator.network.model.NetworkMemberWithSsh;
+import io.getambassador.v2.Mapping;
+import io.getambassador.v2.MappingBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+
+@KubernetesDependent(resourceDiscriminator = GerritClusterMappingDiscriminator.class)
+public class GerritClusterMapping extends AbstractAmbassadorDependentResource<Mapping>
+    implements MappingDependentResourceInterface {
+
+  public static final String GERRIT_MAPPING = "gerrit-mapping";
+
+  public GerritClusterMapping() {
+    super(Mapping.class);
+  }
+
+  @Override
+  public Mapping desired(GerritNetwork gerritNetwork, Context<GerritNetwork> context) {
+
+    // If only one Gerrit instance in GerritCluster, send all git-over-https requests to it
+    NetworkMemberWithSsh gerrit =
+        gerritNetwork.hasGerritReplica()
+            ? gerritNetwork.getSpec().getGerritReplica()
+            : gerritNetwork.getSpec().getPrimaryGerrit();
+    String serviceName = gerrit.getName() + ":" + gerrit.getHttpPort();
+    Mapping mapping =
+        new MappingBuilder()
+            .withNewMetadataLike(
+                getCommonMetadata(gerritNetwork, GERRIT_MAPPING, this.getClass().getSimpleName()))
+            .endMetadata()
+            .withNewSpecLike(getCommonSpec(gerritNetwork, serviceName))
+            .endSpec()
+            .build();
+    return mapping;
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingDiscriminator.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingDiscriminator.java
new file mode 100644
index 0000000..e4bd776
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingDiscriminator.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMapping.GERRIT_MAPPING;
+
+import com.google.gerrit.k8s.operator.network.model.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.Optional;
+
+public class GerritClusterMappingDiscriminator
+    implements ResourceDiscriminator<Mapping, GerritNetwork> {
+  @Override
+  public Optional<Mapping> distinguish(
+      Class<Mapping> resource, GerritNetwork network, Context<GerritNetwork> context) {
+    InformerEventSource<Mapping, GerritNetwork> ies =
+        (InformerEventSource<Mapping, GerritNetwork>)
+            context.eventSourceRetriever().getResourceEventSourceFor(Mapping.class);
+    return ies.get(new ResourceID(GERRIT_MAPPING, network.getMetadata().getNamespace()));
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingGETReplica.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingGETReplica.java
new file mode 100644
index 0000000..6619c75
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingGETReplica.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.gerrit.k8s.operator.network.Constants.INFO_REFS_PATTERN;
+
+import com.google.gerrit.k8s.operator.network.model.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.getambassador.v2.MappingBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+import java.util.HashMap;
+
+@KubernetesDependent(resourceDiscriminator = GerritClusterMappingGETReplicaDiscriminator.class)
+public class GerritClusterMappingGETReplica extends AbstractAmbassadorDependentResource<Mapping>
+    implements MappingDependentResourceInterface {
+
+  public static final String GERRIT_MAPPING_GET_REPLICA = "gerrit-mapping-get-replica";
+
+  public GerritClusterMappingGETReplica() {
+    super(Mapping.class);
+  }
+
+  @Override
+  public Mapping desired(GerritNetwork gerritNetwork, Context<GerritNetwork> context) {
+
+    String replicaServiceName =
+        gerritNetwork.getSpec().getGerritReplica().getName()
+            + ":"
+            + gerritNetwork.getSpec().getGerritReplica().getHttpPort();
+
+    // Send fetch/clone GET requests to the Replica
+    Mapping mapping =
+        new MappingBuilder()
+            .withNewMetadataLike(
+                getCommonMetadata(
+                    gerritNetwork, GERRIT_MAPPING_GET_REPLICA, this.getClass().getSimpleName()))
+            .endMetadata()
+            .withNewSpecLike(getCommonSpec(gerritNetwork, replicaServiceName))
+            .withNewV2QueryParameters()
+            .withAdditionalProperties(
+                new HashMap<String, Object>() {
+                  {
+                    put("service", "git-upload-pack");
+                  }
+                })
+            .endV2QueryParameters()
+            .withMethod("GET")
+            .withPrefix(INFO_REFS_PATTERN)
+            .withPrefixRegex(true)
+            .endSpec()
+            .build();
+
+    return mapping;
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingGETReplicaDiscriminator.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingGETReplicaDiscriminator.java
new file mode 100644
index 0000000..42dd429
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingGETReplicaDiscriminator.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingGETReplica.GERRIT_MAPPING_GET_REPLICA;
+
+import com.google.gerrit.k8s.operator.network.model.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.Optional;
+
+public class GerritClusterMappingGETReplicaDiscriminator
+    implements ResourceDiscriminator<Mapping, GerritNetwork> {
+  @Override
+  public Optional<Mapping> distinguish(
+      Class<Mapping> resource, GerritNetwork network, Context<GerritNetwork> context) {
+    InformerEventSource<Mapping, GerritNetwork> ies =
+        (InformerEventSource<Mapping, GerritNetwork>)
+            context.eventSourceRetriever().getResourceEventSourceFor(Mapping.class);
+    return ies.get(
+        new ResourceID(GERRIT_MAPPING_GET_REPLICA, network.getMetadata().getNamespace()));
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPOSTReplica.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPOSTReplica.java
new file mode 100644
index 0000000..5274f50
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPOSTReplica.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.gerrit.k8s.operator.network.Constants.UPLOAD_PACK_URL_PATTERN;
+
+import com.google.gerrit.k8s.operator.network.model.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.getambassador.v2.MappingBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+
+@KubernetesDependent(resourceDiscriminator = GerritClusterMappingPOSTReplicaDiscriminator.class)
+public class GerritClusterMappingPOSTReplica extends AbstractAmbassadorDependentResource<Mapping>
+    implements MappingDependentResourceInterface {
+
+  public static final String GERRIT_MAPPING_POST_REPLICA = "gerrit-mapping-post-replica";
+
+  public GerritClusterMappingPOSTReplica() {
+    super(Mapping.class);
+  }
+
+  @Override
+  public Mapping desired(GerritNetwork gerritNetwork, Context<GerritNetwork> context) {
+
+    String replicaServiceName =
+        gerritNetwork.getSpec().getGerritReplica().getName()
+            + ":"
+            + gerritNetwork.getSpec().getGerritReplica().getHttpPort();
+
+    // Send fetch/clone POST requests to the Replica
+    Mapping mapping =
+        new MappingBuilder()
+            .withNewMetadataLike(
+                getCommonMetadata(
+                    gerritNetwork, GERRIT_MAPPING_POST_REPLICA, this.getClass().getSimpleName()))
+            .endMetadata()
+            .withNewSpecLike(getCommonSpec(gerritNetwork, replicaServiceName))
+            .withPrefix(UPLOAD_PACK_URL_PATTERN)
+            .withPrefixRegex(true)
+            .withMethod("POST")
+            .endSpec()
+            .build();
+    return mapping;
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPOSTReplicaDiscriminator.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPOSTReplicaDiscriminator.java
new file mode 100644
index 0000000..cb1b8d9
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPOSTReplicaDiscriminator.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingPOSTReplica.GERRIT_MAPPING_POST_REPLICA;
+
+import com.google.gerrit.k8s.operator.network.model.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.Optional;
+
+public class GerritClusterMappingPOSTReplicaDiscriminator
+    implements ResourceDiscriminator<Mapping, GerritNetwork> {
+  @Override
+  public Optional<Mapping> distinguish(
+      Class<Mapping> resource, GerritNetwork network, Context<GerritNetwork> context) {
+    InformerEventSource<Mapping, GerritNetwork> ies =
+        (InformerEventSource<Mapping, GerritNetwork>)
+            context.eventSourceRetriever().getResourceEventSourceFor(Mapping.class);
+    return ies.get(
+        new ResourceID(GERRIT_MAPPING_POST_REPLICA, network.getMetadata().getNamespace()));
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPrimary.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPrimary.java
new file mode 100644
index 0000000..0c93683
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPrimary.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import com.google.gerrit.k8s.operator.network.model.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.getambassador.v2.MappingBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+
+@KubernetesDependent(resourceDiscriminator = GerritClusterMappingPrimaryDiscriminator.class)
+public class GerritClusterMappingPrimary extends AbstractAmbassadorDependentResource<Mapping>
+    implements MappingDependentResourceInterface {
+
+  public static final String GERRIT_MAPPING_PRIMARY = "gerrit-mapping-primary";
+
+  public GerritClusterMappingPrimary() {
+    super(Mapping.class);
+  }
+
+  @Override
+  public Mapping desired(GerritNetwork gerritNetwork, Context<GerritNetwork> context) {
+
+    String primaryServiceName =
+        gerritNetwork.getSpec().getPrimaryGerrit().getName()
+            + ":"
+            + gerritNetwork.getSpec().getPrimaryGerrit().getHttpPort();
+
+    // Send all write traffic (non git fetch/clone traffic) to the Primary.
+    // Emissary evaluates more constrained Mappings first.
+    Mapping mapping =
+        new MappingBuilder()
+            .withNewMetadataLike(
+                getCommonMetadata(
+                    gerritNetwork, GERRIT_MAPPING_PRIMARY, this.getClass().getSimpleName()))
+            .endMetadata()
+            .withNewSpecLike(getCommonSpec(gerritNetwork, primaryServiceName))
+            .endSpec()
+            .build();
+
+    return mapping;
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPrimaryDiscriminator.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPrimaryDiscriminator.java
new file mode 100644
index 0000000..abb7b90
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPrimaryDiscriminator.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingPrimary.GERRIT_MAPPING_PRIMARY;
+
+import com.google.gerrit.k8s.operator.network.model.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.Optional;
+
+public class GerritClusterMappingPrimaryDiscriminator
+    implements ResourceDiscriminator<Mapping, GerritNetwork> {
+  @Override
+  public Optional<Mapping> distinguish(
+      Class<Mapping> resource, GerritNetwork network, Context<GerritNetwork> context) {
+    InformerEventSource<Mapping, GerritNetwork> ies =
+        (InformerEventSource<Mapping, GerritNetwork>)
+            context.eventSourceRetriever().getResourceEventSourceFor(Mapping.class);
+    return ies.get(new ResourceID(GERRIT_MAPPING_PRIMARY, network.getMetadata().getNamespace()));
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiver.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiver.java
new file mode 100644
index 0000000..a35ac1f
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiver.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.gerrit.k8s.operator.network.Constants.PROJECTS_URL_PATTERN;
+import static com.google.gerrit.k8s.operator.network.Constants.RECEIVE_PACK_URL_PATTERN;
+
+import com.google.gerrit.k8s.operator.network.model.GerritNetwork;
+import com.google.gerrit.k8s.operator.receiver.dependent.ReceiverService;
+import io.getambassador.v2.Mapping;
+import io.getambassador.v2.MappingBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+
+@KubernetesDependent(resourceDiscriminator = GerritClusterMappingReceiverDiscriminator.class)
+public class GerritClusterMappingReceiver extends AbstractAmbassadorDependentResource<Mapping>
+    implements MappingDependentResourceInterface {
+
+  public static final String GERRIT_MAPPING_RECEIVER = "gerrit-mapping-receiver";
+
+  public GerritClusterMappingReceiver() {
+    super(Mapping.class);
+  }
+
+  @Override
+  public Mapping desired(GerritNetwork gerritNetwork, Context<GerritNetwork> context) {
+
+    String receiverServiceName =
+        ReceiverService.getName(gerritNetwork.getSpec().getReceiver().getName())
+            + ":"
+            + gerritNetwork.getSpec().getReceiver().getHttpPort();
+
+    Mapping mapping =
+        new MappingBuilder()
+            .withNewMetadataLike(
+                getCommonMetadata(
+                    gerritNetwork, GERRIT_MAPPING_RECEIVER, this.getClass().getSimpleName()))
+            .endMetadata()
+            .withNewSpecLike(getCommonSpec(gerritNetwork, receiverServiceName))
+            .withPrefix(PROJECTS_URL_PATTERN + "|" + RECEIVE_PACK_URL_PATTERN)
+            .withPrefixRegex(true)
+            .endSpec()
+            .build();
+    return mapping;
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiverDiscriminator.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiverDiscriminator.java
new file mode 100644
index 0000000..f5aa07f
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiverDiscriminator.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingReceiver.GERRIT_MAPPING_RECEIVER;
+
+import com.google.gerrit.k8s.operator.network.model.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.Optional;
+
+public class GerritClusterMappingReceiverDiscriminator
+    implements ResourceDiscriminator<Mapping, GerritNetwork> {
+  @Override
+  public Optional<Mapping> distinguish(
+      Class<Mapping> resource, GerritNetwork network, Context<GerritNetwork> context) {
+    InformerEventSource<Mapping, GerritNetwork> ies =
+        (InformerEventSource<Mapping, GerritNetwork>)
+            context.eventSourceRetriever().getResourceEventSourceFor(Mapping.class);
+    return ies.get(new ResourceID(GERRIT_MAPPING_RECEIVER, network.getMetadata().getNamespace()));
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiverGET.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiverGET.java
new file mode 100644
index 0000000..7f59488
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiverGET.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.gerrit.k8s.operator.network.Constants.INFO_REFS_PATTERN;
+
+import com.google.gerrit.k8s.operator.network.model.GerritNetwork;
+import com.google.gerrit.k8s.operator.receiver.dependent.ReceiverService;
+import io.getambassador.v2.Mapping;
+import io.getambassador.v2.MappingBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+import java.util.HashMap;
+
+@KubernetesDependent(resourceDiscriminator = GerritClusterMappingReceiverGETDiscriminator.class)
+public class GerritClusterMappingReceiverGET extends AbstractAmbassadorDependentResource<Mapping>
+    implements MappingDependentResourceInterface {
+
+  public static final String GERRIT_MAPPING_RECEIVER_GET = "gerrit-mapping-receiver-get";
+
+  public GerritClusterMappingReceiverGET() {
+    super(Mapping.class);
+  }
+
+  @Override
+  public Mapping desired(GerritNetwork gerritNetwork, Context<GerritNetwork> context) {
+
+    String receiverServiceName =
+        ReceiverService.getName(gerritNetwork.getSpec().getReceiver().getName())
+            + ":"
+            + gerritNetwork.getSpec().getReceiver().getHttpPort();
+
+    Mapping mapping =
+        new MappingBuilder()
+            .withNewMetadataLike(
+                getCommonMetadata(
+                    gerritNetwork, GERRIT_MAPPING_RECEIVER_GET, this.getClass().getSimpleName()))
+            .endMetadata()
+            .withNewSpecLike(getCommonSpec(gerritNetwork, receiverServiceName))
+            .withNewV2QueryParameters()
+            .withAdditionalProperties(
+                new HashMap<String, Object>() {
+                  {
+                    put("service", "git-receive-pack");
+                  }
+                })
+            .endV2QueryParameters()
+            .withMethod("GET")
+            .withPrefix(INFO_REFS_PATTERN)
+            .withPrefixRegex(true)
+            .endSpec()
+            .build();
+    return mapping;
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiverGETDiscriminator.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiverGETDiscriminator.java
new file mode 100644
index 0000000..27b36e8
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiverGETDiscriminator.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingReceiverGET.GERRIT_MAPPING_RECEIVER_GET;
+
+import com.google.gerrit.k8s.operator.network.model.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.Optional;
+
+public class GerritClusterMappingReceiverGETDiscriminator
+    implements ResourceDiscriminator<Mapping, GerritNetwork> {
+  @Override
+  public Optional<Mapping> distinguish(
+      Class<Mapping> resource, GerritNetwork network, Context<GerritNetwork> context) {
+    InformerEventSource<Mapping, GerritNetwork> ies =
+        (InformerEventSource<Mapping, GerritNetwork>)
+            context.eventSourceRetriever().getResourceEventSourceFor(Mapping.class);
+    return ies.get(
+        new ResourceID(GERRIT_MAPPING_RECEIVER_GET, network.getMetadata().getNamespace()));
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/LoadBalanceCondition.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/LoadBalanceCondition.java
new file mode 100644
index 0000000..6f9b3b1
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/LoadBalanceCondition.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import com.google.gerrit.k8s.operator.network.model.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
+
+public class LoadBalanceCondition implements Condition<Mapping, GerritNetwork> {
+
+  @Override
+  public boolean isMet(
+      DependentResource<Mapping, GerritNetwork> dependentResource,
+      GerritNetwork gerritNetwork,
+      Context<GerritNetwork> context) {
+
+    return gerritNetwork.getSpec().getIngress().isEnabled()
+        && gerritNetwork.hasPrimaryGerrit()
+        && gerritNetwork.hasGerritReplica();
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/MappingDependentResourceInterface.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/MappingDependentResourceInterface.java
new file mode 100644
index 0000000..097e972
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/MappingDependentResourceInterface.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import com.google.gerrit.k8s.operator.network.model.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+
+public interface MappingDependentResourceInterface {
+  public Mapping desired(GerritNetwork gerritNetwork, Context<GerritNetwork> context);
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/ReceiverMappingCondition.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/ReceiverMappingCondition.java
new file mode 100644
index 0000000..b9f88fd
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/ReceiverMappingCondition.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import com.google.gerrit.k8s.operator.network.model.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
+
+public class ReceiverMappingCondition implements Condition<Mapping, GerritNetwork> {
+
+  @Override
+  public boolean isMet(
+      DependentResource<Mapping, GerritNetwork> dependentResource,
+      GerritNetwork gerritNetwork,
+      Context<GerritNetwork> context) {
+
+    return gerritNetwork.getSpec().getIngress().isEnabled() && gerritNetwork.hasReceiver();
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/SingleMappingCondition.java b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/SingleMappingCondition.java
new file mode 100644
index 0000000..07606c5
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/SingleMappingCondition.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import com.google.gerrit.k8s.operator.network.model.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
+
+public class SingleMappingCondition implements Condition<Mapping, GerritNetwork> {
+
+  @Override
+  public boolean isMet(
+      DependentResource<Mapping, GerritNetwork> dependentResource,
+      GerritNetwork gerritNetwork,
+      Context<GerritNetwork> context) {
+
+    return gerritNetwork.getSpec().getIngress().isEnabled()
+        && (gerritNetwork.hasPrimaryGerrit() ^ gerritNetwork.hasGerritReplica());
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/model/ReceiverTemplate.java b/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/model/ReceiverTemplate.java
index 5c4332a..eb932eb 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/model/ReceiverTemplate.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/model/ReceiverTemplate.java
@@ -33,7 +33,7 @@
 @Buildable(
     editableEnabled = false,
     validationEnabled = false,
-    generateBuilderPackage = true,
+    generateBuilderPackage = false,
     lazyCollectionInitEnabled = false,
     builderPackage = "io.fabric8.kubernetes.api.builder")
 public class ReceiverTemplate implements KubernetesResource {
diff --git a/operator/src/main/resources/crd/emissary-crds.yaml b/operator/src/main/resources/crd/emissary-crds.yaml
new file mode 100644
index 0000000..5ead04e
--- /dev/null
+++ b/operator/src/main/resources/crd/emissary-crds.yaml
@@ -0,0 +1,1952 @@
+# This file is downloaded from the Emissary repository on GitHub:
+# https://github.com/emissary-ingress/emissary/blob/master/manifests/emissary/emissary-crds.yaml.in
+#
+# Several modifications have been manually made:
+# 1. Only the `Mapping` and `TLSContext` CRDs have been kept from the source file. The source file
+#    defines many CRDs that are not required by this operator project so the unnecessary CRDs have
+#    been deleted.
+# 2. `v2ExplicitTLS` field has been removed from the Mapping CRD `v3alpha1` version. This is because
+#    the "crd-to-java" generator plugin we use has a bug (https://github.com/fabric8io/kubernetes-client/issues/5457)
+#    while converting enum types and the bug is triggered by the `v2ExplicitTLS` field. This field
+#    may be added back in once we upgrade our fabric8 version to 6.8.x, where this bug is resolved.
+# 3. `ambassador_id` property is added to `Mapping` and `TLSContext` CRD version `v2`, by copying it
+#    over from `v3`.
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.12.0
+  labels:
+    app.kubernetes.io/instance: emissary-apiext
+    app.kubernetes.io/managed-by: kubectl_apply_-f_emissary-apiext.yaml
+    app.kubernetes.io/name: emissary-apiext
+    app.kubernetes.io/part-of: emissary-apiext
+  name: mappings.getambassador.io
+spec:
+  conversion:
+    strategy: Webhook
+    webhook:
+      clientConfig:
+        service:
+          name: emissary-apiext
+          namespace: emissary-system
+      conversionReviewVersions:
+      - v1
+  group: getambassador.io
+  names:
+    categories:
+    - ambassador-crds
+    kind: Mapping
+    listKind: MappingList
+    plural: mappings
+    singular: mapping
+  preserveUnknownFields: false
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - jsonPath: .spec.host
+      name: Source Host
+      type: string
+    - jsonPath: .spec.prefix
+      name: Source Prefix
+      type: string
+    - jsonPath: .spec.service
+      name: Dest Service
+      type: string
+    - jsonPath: .status.state
+      name: State
+      type: string
+    - jsonPath: .status.reason
+      name: Reason
+      type: string
+    name: v1
+    schema:
+      openAPIV3Schema:
+        description: Mapping is the Schema for the mappings API
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: MappingSpec defines the desired state of Mapping
+            properties:
+              add_linkerd_headers:
+                type: boolean
+              add_request_headers:
+                type: object
+                x-kubernetes-preserve-unknown-fields: true
+              add_response_headers:
+                type: object
+                x-kubernetes-preserve-unknown-fields: true
+              allow_upgrade:
+                description: "A case-insensitive list of the non-HTTP protocols to
+                  allow \"upgrading\" to from HTTP via the \"Connection: upgrade\"
+                  mechanism[1].  After the upgrade, Ambassador does not interpret
+                  the traffic, and behaves similarly to how it does for TCPMappings.
+                  \n [1]: https://tools.ietf.org/html/rfc7230#section-6.7 \n For example,
+                  if your upstream service supports WebSockets, you would write \n
+                  allow_upgrade: - websocket \n Or if your upstream service supports
+                  upgrading from HTTP to SPDY (as the Kubernetes apiserver does for
+                  `kubectl exec` functionality), you would write \n allow_upgrade:
+                  - spdy/3.1"
+                items:
+                  type: string
+                type: array
+              auth_context_extensions:
+                additionalProperties:
+                  type: string
+                type: object
+              auto_host_rewrite:
+                type: boolean
+              bypass_auth:
+                type: boolean
+              bypass_error_response_overrides:
+                description: If true, bypasses any `error_response_overrides` set
+                  on the Ambassador module.
+                type: boolean
+              case_sensitive:
+                type: boolean
+              circuit_breakers:
+                items:
+                  properties:
+                    max_connections:
+                      type: integer
+                    max_pending_requests:
+                      type: integer
+                    max_requests:
+                      type: integer
+                    max_retries:
+                      type: integer
+                    priority:
+                      enum:
+                      - default
+                      - high
+                      type: string
+                  type: object
+                type: array
+              cluster_idle_timeout_ms:
+                type: integer
+              cluster_max_connection_lifetime_ms:
+                type: integer
+              cluster_tag:
+                type: string
+              connect_timeout_ms:
+                type: integer
+              cors:
+                properties:
+                  credentials:
+                    type: boolean
+                  max_age:
+                    type: string
+                type: object
+                x-kubernetes-preserve-unknown-fields: true
+              dns_type:
+                type: string
+              docs:
+                description: DocsInfo provides some extra information about the docs
+                  for the Mapping (used by the Dev Portal)
+                properties:
+                  display_name:
+                    type: string
+                  ignored:
+                    type: boolean
+                  path:
+                    type: string
+                  timeout_ms:
+                    type: integer
+                  url:
+                    type: string
+                type: object
+              enable_ipv4:
+                type: boolean
+              enable_ipv6:
+                type: boolean
+              envoy_override:
+                type: object
+                x-kubernetes-preserve-unknown-fields: true
+              error_response_overrides:
+                description: Error response overrides for this Mapping. Replaces all
+                  of the `error_response_overrides` set on the Ambassador module,
+                  if any.
+                items:
+                  description: A response rewrite for an HTTP error response
+                  properties:
+                    body:
+                      description: The new response body
+                      properties:
+                        content_type:
+                          description: The content type to set on the error response
+                            body when using text_format or text_format_source. Defaults
+                            to 'text/plain'.
+                          type: string
+                        json_format:
+                          additionalProperties:
+                            type: string
+                          description: 'A JSON response with content-type: application/json.
+                            The values can contain format text like in text_format.'
+                          type: object
+                        text_format:
+                          description: A format string representing a text response
+                            body. Content-Type can be set using the `content_type`
+                            field below.
+                          type: string
+                        text_format_source:
+                          description: A format string sourced from a file on the
+                            Ambassador container. Useful for larger response bodies
+                            that should not be placed inline in configuration.
+                          properties:
+                            filename:
+                              description: The name of a file on the Ambassador pod
+                                that contains a format text string.
+                              type: string
+                          type: object
+                      type: object
+                    on_status_code:
+                      description: The status code to match on -- not a pointer because
+                        it's required.
+                      maximum: 599
+                      minimum: 400
+                      type: integer
+                  required:
+                  - body
+                  - on_status_code
+                  type: object
+                minItems: 1
+                type: array
+              grpc:
+                type: boolean
+              headers:
+                type: object
+                x-kubernetes-preserve-unknown-fields: true
+              host:
+                type: string
+              host_redirect:
+                type: boolean
+              host_regex:
+                type: boolean
+              host_rewrite:
+                type: string
+              idle_timeout_ms:
+                type: integer
+              keepalive:
+                properties:
+                  idle_time:
+                    type: integer
+                  interval:
+                    type: integer
+                  probes:
+                    type: integer
+                type: object
+              labels:
+                additionalProperties:
+                  description: A MappingLabelGroupsArray is an array of MappingLabelGroups.
+                    I know, complex.
+                  items:
+                    description: 'A MappingLabelGroup is a single element of a MappingLabelGroupsArray:
+                      a second map, where the key is a human-readable name that identifies
+                      the group.'
+                    maxProperties: 1
+                    minProperties: 1
+                    type: object
+                    x-kubernetes-preserve-unknown-fields: true
+                  type: array
+                description: A DomainMap is the overall Mapping.spec.Labels type.
+                  It maps domains (kind of like namespaces for Mapping labels) to
+                  arrays of label groups.
+                type: object
+              load_balancer:
+                properties:
+                  cookie:
+                    properties:
+                      name:
+                        type: string
+                      path:
+                        type: string
+                      ttl:
+                        type: string
+                    required:
+                    - name
+                    type: object
+                  header:
+                    type: string
+                  policy:
+                    enum:
+                    - round_robin
+                    - ring_hash
+                    - maglev
+                    - least_request
+                    type: string
+                  source_ip:
+                    type: boolean
+                required:
+                - policy
+                type: object
+              method:
+                type: string
+              method_regex:
+                type: boolean
+              modules:
+                items:
+                  type: object
+                  x-kubernetes-preserve-unknown-fields: true
+                type: array
+              outlier_detection:
+                type: string
+              path_redirect:
+                description: Path replacement to use when generating an HTTP redirect.
+                  Used with `host_redirect`.
+                type: string
+              precedence:
+                type: integer
+              prefix:
+                type: string
+              prefix_exact:
+                type: boolean
+              prefix_redirect:
+                description: Prefix rewrite to use when generating an HTTP redirect.
+                  Used with `host_redirect`.
+                type: string
+              prefix_regex:
+                type: boolean
+              priority:
+                type: string
+              query_parameters:
+                type: object
+                x-kubernetes-preserve-unknown-fields: true
+              redirect_response_code:
+                description: The response code to use when generating an HTTP redirect.
+                  Defaults to 301. Used with `host_redirect`.
+                enum:
+                - 301
+                - 302
+                - 303
+                - 307
+                - 308
+                type: integer
+              regex_headers:
+                additionalProperties:
+                  type: string
+                type: object
+              regex_query_parameters:
+                additionalProperties:
+                  type: string
+                type: object
+              regex_redirect:
+                description: Prefix regex rewrite to use when generating an HTTP redirect.
+                  Used with `host_redirect`.
+                properties:
+                  pattern:
+                    type: string
+                  substitution:
+                    type: string
+                type: object
+              regex_rewrite:
+                properties:
+                  pattern:
+                    type: string
+                  substitution:
+                    type: string
+                type: object
+              resolver:
+                type: string
+              respect_dns_ttl:
+                type: boolean
+              retry_policy:
+                properties:
+                  num_retries:
+                    type: integer
+                  per_try_timeout:
+                    type: string
+                  retry_on:
+                    enum:
+                    - 5xx
+                    - gateway-error
+                    - connect-failure
+                    - retriable-4xx
+                    - refused-stream
+                    - retriable-status-codes
+                    type: string
+                type: object
+              rewrite:
+                type: string
+              service:
+                type: string
+              shadow:
+                type: boolean
+              timeout_ms:
+                description: The timeout for requests that use this Mapping. Overrides
+                  `cluster_request_timeout_ms` set on the Ambassador Module, if it
+                  exists.
+                type: integer
+              use_websocket:
+                description: 'use_websocket is deprecated, and is equivlaent to setting
+                  `allow_upgrade: ["websocket"]`'
+                type: boolean
+              v3StatsName:
+                type: string
+              v3health_checks:
+                items:
+                  description: HealthCheck specifies settings for performing active
+                    health checking on upstreams
+                  properties:
+                    health_check:
+                      description: Configuration for where the healthcheck request
+                        should be made to
+                      maxProperties: 1
+                      minProperties: 1
+                      properties:
+                        grpc:
+                          description: HealthCheck for gRPC upstreams. Only one of
+                            grpc_health_check or http_health_check may be specified
+                          properties:
+                            authority:
+                              description: The value of the :authority header in the
+                                gRPC health check request. If left empty the upstream
+                                name will be used.
+                              type: string
+                            upstream_name:
+                              description: The upstream name parameter which will
+                                be sent to gRPC service in the health check message
+                              type: string
+                          required:
+                          - upstream_name
+                          type: object
+                        http:
+                          description: HealthCheck for HTTP upstreams. Only one of
+                            http_health_check or grpc_health_check may be specified
+                          properties:
+                            add_request_headers:
+                              additionalProperties:
+                                properties:
+                                  append:
+                                    type: boolean
+                                  v2Representation:
+                                    enum:
+                                    - ""
+                                    - string
+                                    - "null"
+                                    type: string
+                                  value:
+                                    type: string
+                                type: object
+                              type: object
+                            expected_statuses:
+                              items:
+                                description: A range of response statuses from Start
+                                  to End inclusive
+                                properties:
+                                  max:
+                                    description: End of the statuses to include. Must
+                                      be between 100 and 599 (inclusive)
+                                    maximum: 599
+                                    minimum: 100
+                                    type: integer
+                                  min:
+                                    description: Start of the statuses to include.
+                                      Must be between 100 and 599 (inclusive)
+                                    maximum: 599
+                                    minimum: 100
+                                    type: integer
+                                required:
+                                - max
+                                - min
+                                type: object
+                              type: array
+                            hostname:
+                              type: string
+                            path:
+                              type: string
+                            remove_request_headers:
+                              items:
+                                type: string
+                              type: array
+                          required:
+                          - path
+                          type: object
+                      type: object
+                    healthy_threshold:
+                      description: Number of expected responses for the upstream to
+                        be considered healthy. Defaults to 1.
+                      type: integer
+                    interval:
+                      description: Interval between health checks. Defaults to every
+                        5 seconds.
+                      type: string
+                    timeout:
+                      description: Timeout for connecting to the health checking endpoint.
+                        Defaults to 3 seconds.
+                      type: string
+                    unhealthy_threshold:
+                      description: Number of non-expected responses for the upstream
+                        to be considered unhealthy. A single 503 will mark the upstream
+                        as unhealthy regardless of the threshold. Defaults to 2.
+                      type: integer
+                  required:
+                  - health_check
+                  type: object
+                minItems: 1
+                type: array
+              weight:
+                type: integer
+            required:
+            - prefix
+            - service
+            type: object
+            x-kubernetes-preserve-unknown-fields: true
+          status:
+            description: MappingStatus defines the observed state of Mapping
+            properties:
+              reason:
+                type: string
+              state:
+                enum:
+                - ""
+                - Inactive
+                - Running
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: false
+    subresources:
+      status: {}
+  - additionalPrinterColumns:
+    - jsonPath: .spec.host
+      name: Source Host
+      type: string
+    - jsonPath: .spec.prefix
+      name: Source Prefix
+      type: string
+    - jsonPath: .spec.service
+      name: Dest Service
+      type: string
+    - jsonPath: .status.state
+      name: State
+      type: string
+    - jsonPath: .status.reason
+      name: Reason
+      type: string
+    name: v2
+    schema:
+      openAPIV3Schema:
+        description: Mapping is the Schema for the mappings API
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: MappingSpec defines the desired state of Mapping
+            properties:
+              add_linkerd_headers:
+                type: boolean
+              add_request_headers:
+                type: object
+                x-kubernetes-preserve-unknown-fields: true
+              add_response_headers:
+                type: object
+                x-kubernetes-preserve-unknown-fields: true
+              allow_upgrade:
+                description: "A case-insensitive list of the non-HTTP protocols to
+                  allow \"upgrading\" to from HTTP via the \"Connection: upgrade\"
+                  mechanism[1].  After the upgrade, Ambassador does not interpret
+                  the traffic, and behaves similarly to how it does for TCPMappings.
+                  \n [1]: https://tools.ietf.org/html/rfc7230#section-6.7 \n For example,
+                  if your upstream service supports WebSockets, you would write \n
+                  allow_upgrade: - websocket \n Or if your upstream service supports
+                  upgrading from HTTP to SPDY (as the Kubernetes apiserver does for
+                  `kubectl exec` functionality), you would write \n allow_upgrade:
+                  - spdy/3.1"
+                items:
+                  type: string
+                type: array
+              # [operator] added manually by coping over from v3alpha1
+              ambassador_id:
+                description: "AmbassadorID declares which Ambassador instances should
+                  pay attention to this resource. If no value is provided, the default
+                  is: \n ambassador_id: - \"default\""
+                items:
+                  type: string
+                type: array
+              auth_context_extensions:
+                additionalProperties:
+                  type: string
+                type: object
+              auto_host_rewrite:
+                type: boolean
+              bypass_auth:
+                type: boolean
+              bypass_error_response_overrides:
+                description: If true, bypasses any `error_response_overrides` set
+                  on the Ambassador module.
+                type: boolean
+              case_sensitive:
+                type: boolean
+              circuit_breakers:
+                items:
+                  properties:
+                    max_connections:
+                      type: integer
+                    max_pending_requests:
+                      type: integer
+                    max_requests:
+                      type: integer
+                    max_retries:
+                      type: integer
+                    priority:
+                      enum:
+                      - default
+                      - high
+                      type: string
+                  type: object
+                type: array
+              cluster_idle_timeout_ms:
+                type: integer
+              cluster_max_connection_lifetime_ms:
+                type: integer
+              cluster_tag:
+                type: string
+              connect_timeout_ms:
+                type: integer
+              cors:
+                properties:
+                  credentials:
+                    type: boolean
+                  max_age:
+                    type: string
+                type: object
+                x-kubernetes-preserve-unknown-fields: true
+              dns_type:
+                type: string
+              docs:
+                description: DocsInfo provides some extra information about the docs
+                  for the Mapping (used by the Dev Portal)
+                properties:
+                  display_name:
+                    type: string
+                  ignored:
+                    type: boolean
+                  path:
+                    type: string
+                  timeout_ms:
+                    type: integer
+                  url:
+                    type: string
+                type: object
+              enable_ipv4:
+                type: boolean
+              enable_ipv6:
+                type: boolean
+              envoy_override:
+                type: object
+                x-kubernetes-preserve-unknown-fields: true
+              error_response_overrides:
+                description: Error response overrides for this Mapping. Replaces all
+                  of the `error_response_overrides` set on the Ambassador module,
+                  if any.
+                items:
+                  description: A response rewrite for an HTTP error response
+                  properties:
+                    body:
+                      description: The new response body
+                      properties:
+                        content_type:
+                          description: The content type to set on the error response
+                            body when using text_format or text_format_source. Defaults
+                            to 'text/plain'.
+                          type: string
+                        json_format:
+                          additionalProperties:
+                            type: string
+                          description: 'A JSON response with content-type: application/json.
+                            The values can contain format text like in text_format.'
+                          type: object
+                        text_format:
+                          description: A format string representing a text response
+                            body. Content-Type can be set using the `content_type`
+                            field below.
+                          type: string
+                        text_format_source:
+                          description: A format string sourced from a file on the
+                            Ambassador container. Useful for larger response bodies
+                            that should not be placed inline in configuration.
+                          properties:
+                            filename:
+                              description: The name of a file on the Ambassador pod
+                                that contains a format text string.
+                              type: string
+                          type: object
+                      type: object
+                    on_status_code:
+                      description: The status code to match on -- not a pointer because
+                        it's required.
+                      maximum: 599
+                      minimum: 400
+                      type: integer
+                  required:
+                  - body
+                  - on_status_code
+                  type: object
+                minItems: 1
+                type: array
+              grpc:
+                type: boolean
+              headers:
+                type: object
+                x-kubernetes-preserve-unknown-fields: true
+              host:
+                type: string
+              host_redirect:
+                type: boolean
+              host_regex:
+                type: boolean
+              host_rewrite:
+                type: string
+              idle_timeout_ms:
+                type: integer
+              keepalive:
+                properties:
+                  idle_time:
+                    type: integer
+                  interval:
+                    type: integer
+                  probes:
+                    type: integer
+                type: object
+              labels:
+                additionalProperties:
+                  description: A MappingLabelGroupsArray is an array of MappingLabelGroups.
+                    I know, complex.
+                  items:
+                    description: 'A MappingLabelGroup is a single element of a MappingLabelGroupsArray:
+                      a second map, where the key is a human-readable name that identifies
+                      the group.'
+                    maxProperties: 1
+                    minProperties: 1
+                    type: object
+                    x-kubernetes-preserve-unknown-fields: true
+                  type: array
+                description: A DomainMap is the overall Mapping.spec.Labels type.
+                  It maps domains (kind of like namespaces for Mapping labels) to
+                  arrays of label groups.
+                type: object
+              load_balancer:
+                properties:
+                  cookie:
+                    properties:
+                      name:
+                        type: string
+                      path:
+                        type: string
+                      ttl:
+                        type: string
+                    required:
+                    - name
+                    type: object
+                  header:
+                    type: string
+                  policy:
+                    enum:
+                    - round_robin
+                    - ring_hash
+                    - maglev
+                    - least_request
+                    type: string
+                  source_ip:
+                    type: boolean
+                required:
+                - policy
+                type: object
+              method:
+                type: string
+              method_regex:
+                type: boolean
+              modules:
+                items:
+                  type: object
+                  x-kubernetes-preserve-unknown-fields: true
+                type: array
+              outlier_detection:
+                type: string
+              path_redirect:
+                description: Path replacement to use when generating an HTTP redirect.
+                  Used with `host_redirect`.
+                type: string
+              precedence:
+                type: integer
+              prefix:
+                type: string
+              prefix_exact:
+                type: boolean
+              prefix_redirect:
+                description: Prefix rewrite to use when generating an HTTP redirect.
+                  Used with `host_redirect`.
+                type: string
+              prefix_regex:
+                type: boolean
+              priority:
+                type: string
+              query_parameters:
+                type: object
+                x-kubernetes-preserve-unknown-fields: true
+              redirect_response_code:
+                description: The response code to use when generating an HTTP redirect.
+                  Defaults to 301. Used with `host_redirect`.
+                enum:
+                - 301
+                - 302
+                - 303
+                - 307
+                - 308
+                type: integer
+              regex_headers:
+                additionalProperties:
+                  type: string
+                type: object
+              regex_query_parameters:
+                additionalProperties:
+                  type: string
+                type: object
+              regex_redirect:
+                description: Prefix regex rewrite to use when generating an HTTP redirect.
+                  Used with `host_redirect`.
+                properties:
+                  pattern:
+                    type: string
+                  substitution:
+                    type: string
+                type: object
+              regex_rewrite:
+                properties:
+                  pattern:
+                    type: string
+                  substitution:
+                    type: string
+                type: object
+              resolver:
+                type: string
+              respect_dns_ttl:
+                type: boolean
+              retry_policy:
+                properties:
+                  num_retries:
+                    type: integer
+                  per_try_timeout:
+                    type: string
+                  retry_on:
+                    enum:
+                    - 5xx
+                    - gateway-error
+                    - connect-failure
+                    - retriable-4xx
+                    - refused-stream
+                    - retriable-status-codes
+                    type: string
+                type: object
+              rewrite:
+                type: string
+              service:
+                type: string
+              shadow:
+                type: boolean
+              timeout_ms:
+                description: The timeout for requests that use this Mapping. Overrides
+                  `cluster_request_timeout_ms` set on the Ambassador Module, if it
+                  exists.
+                type: integer
+              use_websocket:
+                description: 'use_websocket is deprecated, and is equivlaent to setting
+                  `allow_upgrade: ["websocket"]`'
+                type: boolean
+              v3StatsName:
+                type: string
+              v3health_checks:
+                items:
+                  description: HealthCheck specifies settings for performing active
+                    health checking on upstreams
+                  properties:
+                    health_check:
+                      description: Configuration for where the healthcheck request
+                        should be made to
+                      maxProperties: 1
+                      minProperties: 1
+                      properties:
+                        grpc:
+                          description: HealthCheck for gRPC upstreams. Only one of
+                            grpc_health_check or http_health_check may be specified
+                          properties:
+                            authority:
+                              description: The value of the :authority header in the
+                                gRPC health check request. If left empty the upstream
+                                name will be used.
+                              type: string
+                            upstream_name:
+                              description: The upstream name parameter which will
+                                be sent to gRPC service in the health check message
+                              type: string
+                          required:
+                          - upstream_name
+                          type: object
+                        http:
+                          description: HealthCheck for HTTP upstreams. Only one of
+                            http_health_check or grpc_health_check may be specified
+                          properties:
+                            add_request_headers:
+                              additionalProperties:
+                                properties:
+                                  append:
+                                    type: boolean
+                                  v2Representation:
+                                    enum:
+                                    - ""
+                                    - string
+                                    - "null"
+                                    type: string
+                                  value:
+                                    type: string
+                                type: object
+                              type: object
+                            expected_statuses:
+                              items:
+                                description: A range of response statuses from Start
+                                  to End inclusive
+                                properties:
+                                  max:
+                                    description: End of the statuses to include. Must
+                                      be between 100 and 599 (inclusive)
+                                    maximum: 599
+                                    minimum: 100
+                                    type: integer
+                                  min:
+                                    description: Start of the statuses to include.
+                                      Must be between 100 and 599 (inclusive)
+                                    maximum: 599
+                                    minimum: 100
+                                    type: integer
+                                required:
+                                - max
+                                - min
+                                type: object
+                              type: array
+                            hostname:
+                              type: string
+                            path:
+                              type: string
+                            remove_request_headers:
+                              items:
+                                type: string
+                              type: array
+                          required:
+                          - path
+                          type: object
+                      type: object
+                    healthy_threshold:
+                      description: Number of expected responses for the upstream to
+                        be considered healthy. Defaults to 1.
+                      type: integer
+                    interval:
+                      description: Interval between health checks. Defaults to every
+                        5 seconds.
+                      type: string
+                    timeout:
+                      description: Timeout for connecting to the health checking endpoint.
+                        Defaults to 3 seconds.
+                      type: string
+                    unhealthy_threshold:
+                      description: Number of non-expected responses for the upstream
+                        to be considered unhealthy. A single 503 will mark the upstream
+                        as unhealthy regardless of the threshold. Defaults to 2.
+                      type: integer
+                  required:
+                  - health_check
+                  type: object
+                minItems: 1
+                type: array
+              weight:
+                type: integer
+            required:
+            - prefix
+            - service
+            type: object
+            x-kubernetes-preserve-unknown-fields: true
+          status:
+            description: MappingStatus defines the observed state of Mapping
+            properties:
+              reason:
+                type: string
+              state:
+                enum:
+                - ""
+                - Inactive
+                - Running
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+  - additionalPrinterColumns:
+    - jsonPath: .spec.host
+      name: Source Host
+      type: string
+    - jsonPath: .spec.prefix
+      name: Source Prefix
+      type: string
+    - jsonPath: .spec.service
+      name: Dest Service
+      type: string
+    - jsonPath: .status.state
+      name: State
+      type: string
+    - jsonPath: .status.reason
+      name: Reason
+      type: string
+    name: v3alpha1
+    schema:
+      openAPIV3Schema:
+        description: Mapping is the Schema for the mappings API
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: MappingSpec defines the desired state of Mapping
+            properties:
+              add_linkerd_headers:
+                type: boolean
+              add_request_headers:
+                additionalProperties:
+                  properties:
+                    append:
+                      type: boolean
+                    v2Representation:
+                      enum:
+                      - ""
+                      - string
+                      - "null"
+                      type: string
+                    value:
+                      type: string
+                  type: object
+                type: object
+              add_response_headers:
+                additionalProperties:
+                  properties:
+                    append:
+                      type: boolean
+                    v2Representation:
+                      enum:
+                      - ""
+                      - string
+                      - "null"
+                      type: string
+                    value:
+                      type: string
+                  type: object
+                type: object
+              allow_upgrade:
+                description: "A case-insensitive list of the non-HTTP protocols to
+                  allow \"upgrading\" to from HTTP via the \"Connection: upgrade\"
+                  mechanism[1].  After the upgrade, Ambassador does not interpret
+                  the traffic, and behaves similarly to how it does for TCPMappings.
+                  \n [1]: https://tools.ietf.org/html/rfc7230#section-6.7 \n For example,
+                  if your upstream service supports WebSockets, you would write \n
+                  allow_upgrade: - websocket \n Or if your upstream service supports
+                  upgrading from HTTP to SPDY (as the Kubernetes apiserver does for
+                  `kubectl exec` functionality), you would write \n allow_upgrade:
+                  - spdy/3.1"
+                items:
+                  type: string
+                type: array
+              ambassador_id:
+                description: "AmbassadorID declares which Ambassador instances should
+                  pay attention to this resource. If no value is provided, the default
+                  is: \n ambassador_id: - \"default\""
+                items:
+                  type: string
+                type: array
+              auth_context_extensions:
+                additionalProperties:
+                  type: string
+                type: object
+              auto_host_rewrite:
+                type: boolean
+              bypass_auth:
+                type: boolean
+              bypass_error_response_overrides:
+                description: If true, bypasses any `error_response_overrides` set
+                  on the Ambassador module.
+                type: boolean
+              case_sensitive:
+                type: boolean
+              circuit_breakers:
+                items:
+                  properties:
+                    max_connections:
+                      type: integer
+                    max_pending_requests:
+                      type: integer
+                    max_requests:
+                      type: integer
+                    max_retries:
+                      type: integer
+                    priority:
+                      enum:
+                      - default
+                      - high
+                      type: string
+                  type: object
+                type: array
+              cluster_idle_timeout_ms:
+                type: integer
+              cluster_max_connection_lifetime_ms:
+                type: integer
+              cluster_tag:
+                type: string
+              connect_timeout_ms:
+                type: integer
+              cors:
+                properties:
+                  credentials:
+                    type: boolean
+                  exposed_headers:
+                    items:
+                      type: string
+                    type: array
+                  headers:
+                    items:
+                      type: string
+                    type: array
+                  max_age:
+                    type: string
+                  methods:
+                    items:
+                      type: string
+                    type: array
+                  origins:
+                    items:
+                      type: string
+                    type: array
+                  v2CommaSeparatedOrigins:
+                    type: boolean
+                type: object
+              dns_type:
+                type: string
+              docs:
+                description: DocsInfo provides some extra information about the docs
+                  for the Mapping. Docs is used by both the agent and the DevPortal.
+                properties:
+                  display_name:
+                    type: string
+                  ignored:
+                    type: boolean
+                  path:
+                    type: string
+                  timeout_ms:
+                    type: integer
+                  url:
+                    type: string
+                type: object
+              enable_ipv4:
+                type: boolean
+              enable_ipv6:
+                type: boolean
+              envoy_override:
+                type: object
+                x-kubernetes-preserve-unknown-fields: true
+              error_response_overrides:
+                description: Error response overrides for this Mapping. Replaces all
+                  of the `error_response_overrides` set on the Ambassador module,
+                  if any.
+                items:
+                  description: A response rewrite for an HTTP error response
+                  properties:
+                    body:
+                      description: The new response body
+                      properties:
+                        content_type:
+                          description: The content type to set on the error response
+                            body when using text_format or text_format_source. Defaults
+                            to 'text/plain'.
+                          type: string
+                        json_format:
+                          additionalProperties:
+                            type: string
+                          description: 'A JSON response with content-type: application/json.
+                            The values can contain format text like in text_format.'
+                          type: object
+                        text_format:
+                          description: A format string representing a text response
+                            body. Content-Type can be set using the `content_type`
+                            field below.
+                          type: string
+                        text_format_source:
+                          description: A format string sourced from a file on the
+                            Ambassador container. Useful for larger response bodies
+                            that should not be placed inline in configuration.
+                          properties:
+                            filename:
+                              description: The name of a file on the Ambassador pod
+                                that contains a format text string.
+                              type: string
+                          type: object
+                      type: object
+                    on_status_code:
+                      description: The status code to match on -- not a pointer because
+                        it's required.
+                      maximum: 599
+                      minimum: 400
+                      type: integer
+                  required:
+                  - body
+                  - on_status_code
+                  type: object
+                minItems: 1
+                type: array
+              grpc:
+                type: boolean
+              headers:
+                additionalProperties:
+                  type: string
+                type: object
+              health_checks:
+                items:
+                  description: HealthCheck specifies settings for performing active
+                    health checking on upstreams
+                  properties:
+                    health_check:
+                      description: Configuration for where the healthcheck request
+                        should be made to
+                      maxProperties: 1
+                      minProperties: 1
+                      properties:
+                        grpc:
+                          description: HealthCheck for gRPC upstreams. Only one of
+                            grpc_health_check or http_health_check may be specified
+                          properties:
+                            authority:
+                              description: The value of the :authority header in the
+                                gRPC health check request. If left empty the upstream
+                                name will be used.
+                              type: string
+                            upstream_name:
+                              description: The upstream name parameter which will
+                                be sent to gRPC service in the health check message
+                              type: string
+                          required:
+                          - upstream_name
+                          type: object
+                        http:
+                          description: HealthCheck for HTTP upstreams. Only one of
+                            http_health_check or grpc_health_check may be specified
+                          properties:
+                            add_request_headers:
+                              additionalProperties:
+                                properties:
+                                  append:
+                                    type: boolean
+                                  v2Representation:
+                                    enum:
+                                    - ""
+                                    - string
+                                    - "null"
+                                    type: string
+                                  value:
+                                    type: string
+                                type: object
+                              type: object
+                            expected_statuses:
+                              items:
+                                description: A range of response statuses from Start
+                                  to End inclusive
+                                properties:
+                                  max:
+                                    description: End of the statuses to include. Must
+                                      be between 100 and 599 (inclusive)
+                                    maximum: 599
+                                    minimum: 100
+                                    type: integer
+                                  min:
+                                    description: Start of the statuses to include.
+                                      Must be between 100 and 599 (inclusive)
+                                    maximum: 599
+                                    minimum: 100
+                                    type: integer
+                                required:
+                                - max
+                                - min
+                                type: object
+                              type: array
+                            hostname:
+                              type: string
+                            path:
+                              type: string
+                            remove_request_headers:
+                              items:
+                                type: string
+                              type: array
+                          required:
+                          - path
+                          type: object
+                      type: object
+                    healthy_threshold:
+                      description: Number of expected responses for the upstream to
+                        be considered healthy. Defaults to 1.
+                      type: integer
+                    interval:
+                      description: Interval between health checks. Defaults to every
+                        5 seconds.
+                      type: string
+                    timeout:
+                      description: Timeout for connecting to the health checking endpoint.
+                        Defaults to 3 seconds.
+                      type: string
+                    unhealthy_threshold:
+                      description: Number of non-expected responses for the upstream
+                        to be considered unhealthy. A single 503 will mark the upstream
+                        as unhealthy regardless of the threshold. Defaults to 2.
+                      type: integer
+                  required:
+                  - health_check
+                  type: object
+                minItems: 1
+                type: array
+              host:
+                description: "Exact match for the hostname of a request if HostRegex
+                  is false; regex match for the hostname if HostRegex is true. \n
+                  Host specifies both a match for the ':authority' header of a request,
+                  as well as a match criterion for Host CRDs: a Mapping that specifies
+                  Host will not associate with a Host that doesn't have a matching
+                  Hostname. \n If both Host and Hostname are set, an error is logged,
+                  Host is ignored, and Hostname is used. \n DEPRECATED: Host is either
+                  an exact match or a regex, depending on HostRegex. Use HostName
+                  instead."
+                type: string
+              host_redirect:
+                type: boolean
+              host_regex:
+                description: 'DEPRECATED: Host is either an exact match or a regex,
+                  depending on HostRegex. Use HostName instead.'
+                type: boolean
+              host_rewrite:
+                type: string
+              hostname:
+                description: "Hostname is a DNS glob specifying the hosts to which
+                  this Mapping applies. \n Hostname specifies both a match for the
+                  ':authority' header of a request, as well as a match criterion for
+                  Host CRDs: a Mapping that specifies Hostname will not associate
+                  with a Host that doesn't have a matching Hostname. \n If both Host
+                  and Hostname are set, an error is logged, Host is ignored, and Hostname
+                  is used."
+                type: string
+              idle_timeout_ms:
+                type: integer
+              keepalive:
+                properties:
+                  idle_time:
+                    type: integer
+                  interval:
+                    type: integer
+                  probes:
+                    type: integer
+                type: object
+              labels:
+                additionalProperties:
+                  description: A MappingLabelGroupsArray is an array of MappingLabelGroups.
+                    I know, complex.
+                  items:
+                    additionalProperties:
+                      description: 'A MappingLabelsArray is the value in the MappingLabelGroup:
+                        an array of label specifiers.'
+                      items:
+                        description: "A MappingLabelSpecifier (finally!) defines a
+                          single label. \n This mimics envoy/config/route/v3/route_components.proto:RateLimit:Action:action_specifier."
+                        maxProperties: 1
+                        minProperties: 1
+                        properties:
+                          destination_cluster:
+                            description: Sets the label "destination_cluster=«Envoy
+                              destination cluster name»".
+                            properties:
+                              key:
+                                enum:
+                                - destination_cluster
+                                type: string
+                            required:
+                            - key
+                            type: object
+                          generic_key:
+                            description: Sets the label "«key»=«value»" (where by
+                              default «key» is "generic_key").
+                            properties:
+                              key:
+                                description: The default is "generic_key".
+                                type: string
+                              v2Shorthand:
+                                type: boolean
+                              value:
+                                type: string
+                            required:
+                            - value
+                            type: object
+                          remote_address:
+                            description: Sets the label "remote_address=«IP address
+                              of the client»".
+                            properties:
+                              key:
+                                enum:
+                                - remote_address
+                                type: string
+                            required:
+                            - key
+                            type: object
+                          request_headers:
+                            description: If the «header_name» header is set, then
+                              set the label "«key»=«Value of the «header_name» header»";
+                              otherwise skip applying this label group.
+                            properties:
+                              header_name:
+                                type: string
+                              key:
+                                type: string
+                              omit_if_not_present:
+                                type: boolean
+                            required:
+                            - header_name
+                            - key
+                            type: object
+                          source_cluster:
+                            description: Sets the label "source_cluster=«Envoy source
+                              cluster name»".
+                            properties:
+                              key:
+                                enum:
+                                - source_cluster
+                                type: string
+                            required:
+                            - key
+                            type: object
+                        type: object
+                      type: array
+                    description: 'A MappingLabelGroup is a single element of a MappingLabelGroupsArray:
+                      a second map, where the key is a human-readable name that identifies
+                      the group.'
+                    maxProperties: 1
+                    minProperties: 1
+                    type: object
+                  type: array
+                description: A DomainMap is the overall Mapping.spec.Labels type.
+                  It maps domains (kind of like namespaces for Mapping labels) to
+                  arrays of label groups.
+                type: object
+              load_balancer:
+                properties:
+                  cookie:
+                    properties:
+                      name:
+                        type: string
+                      path:
+                        type: string
+                      ttl:
+                        type: string
+                    required:
+                    - name
+                    type: object
+                  header:
+                    type: string
+                  policy:
+                    enum:
+                    - round_robin
+                    - ring_hash
+                    - maglev
+                    - least_request
+                    type: string
+                  source_ip:
+                    type: boolean
+                required:
+                - policy
+                type: object
+              method:
+                type: string
+              method_regex:
+                type: boolean
+              modules:
+                items:
+                  type: object
+                  x-kubernetes-preserve-unknown-fields: true
+                type: array
+              outlier_detection:
+                type: string
+              path_redirect:
+                description: Path replacement to use when generating an HTTP redirect.
+                  Used with `host_redirect`.
+                type: string
+              precedence:
+                type: integer
+              prefix:
+                type: string
+              prefix_exact:
+                type: boolean
+              prefix_redirect:
+                description: Prefix rewrite to use when generating an HTTP redirect.
+                  Used with `host_redirect`.
+                type: string
+              prefix_regex:
+                type: boolean
+              priority:
+                type: string
+              query_parameters:
+                additionalProperties:
+                  type: string
+                type: object
+              redirect_response_code:
+                description: The response code to use when generating an HTTP redirect.
+                  Defaults to 301. Used with `host_redirect`.
+                enum:
+                - 301
+                - 302
+                - 303
+                - 307
+                - 308
+                type: integer
+              regex_headers:
+                additionalProperties:
+                  type: string
+                type: object
+              regex_query_parameters:
+                additionalProperties:
+                  type: string
+                type: object
+              regex_redirect:
+                description: Prefix regex rewrite to use when generating an HTTP redirect.
+                  Used with `host_redirect`.
+                properties:
+                  pattern:
+                    type: string
+                  substitution:
+                    type: string
+                type: object
+              regex_rewrite:
+                properties:
+                  pattern:
+                    type: string
+                  substitution:
+                    type: string
+                type: object
+              remove_request_headers:
+                items:
+                  type: string
+                type: array
+              remove_response_headers:
+                items:
+                  type: string
+                type: array
+              resolver:
+                type: string
+              respect_dns_ttl:
+                type: boolean
+              retry_policy:
+                properties:
+                  num_retries:
+                    type: integer
+                  per_try_timeout:
+                    type: string
+                  retry_on:
+                    enum:
+                    - 5xx
+                    - gateway-error
+                    - connect-failure
+                    - retriable-4xx
+                    - refused-stream
+                    - retriable-status-codes
+                    type: string
+                type: object
+              rewrite:
+                type: string
+              service:
+                type: string
+              shadow:
+                type: boolean
+              stats_name:
+                type: string
+              timeout_ms:
+                description: The timeout for requests that use this Mapping. Overrides
+                  `cluster_request_timeout_ms` set on the Ambassador Module, if it
+                  exists.
+                type: integer
+              tls:
+                type: string
+              use_websocket:
+                description: 'use_websocket is deprecated, and is equivlaent to setting
+                  `allow_upgrade: ["websocket"]`'
+                type: boolean
+              v2BoolHeaders:
+                items:
+                  type: string
+                type: array
+              v2BoolQueryParameters:
+                items:
+                  type: string
+                type: array
+              # TODO: uncomment when [bug](https://github.com/fabric8io/kubernetes-client/issues/5457) is resolved
+              # v2ExplicitTLS:
+              #   description: V2ExplicitTLS controls some vanity/stylistic elements
+              #     when converting from v3alpha1 to v2.  The values in an V2ExplicitTLS
+              #     should not in any way affect the runtime operation of Emissary;
+              #     except that it may affect internal names in the Envoy config, which
+              #     may in turn affect stats names.  But it should not affect any end-user
+              #     observable behavior.
+              #   properties:
+              #     serviceScheme:
+              #       description: "ServiceScheme specifies how to spell and capitalize
+              #         the scheme-part of the service URL. \n Acceptable values are
+              #         \"http://\" (case-insensitive), \"https://\" (case-insensitive),
+              #         or \"\".  The value is used if it agrees with whether or not
+              #         this resource enables TLS origination, or if something else
+              #         in the resource overrides the scheme."
+              #       pattern: ^([hH][tT][tT][pP][sS]?://)?$
+              #       type: string
+              #     tls:
+              #       description: "TLS controls whether and how to represent the \"tls\"
+              #         field when its value could be implied by the \"service\" field.
+              #         \ In v2, there were a lot of different ways to spell an \"empty\"
+              #         value, and this field specifies which way to spell it (and will
+              #         therefore only be used if the value will indeed be empty). \n
+              #         | Value        | Representation                        | Meaning
+              #         of representation          | |--------------+---------------------------------------+------------------------------------|
+              #         | \"\"           | omit the field                        | defer
+              #         to service (no TLSContext)   | | \"null\"       | store an explicit
+              #         \"null\" in the field | defer to service (no TLSContext)   |
+              #         | \"string\"     | store an empty string in the field    | defer
+              #         to service (no TLSContext)   | | \"bool:false\" | store a Boolean
+              #         \"false\" in the field  | defer to service (no TLSContext)   |
+              #         | \"bool:true\"  | store a Boolean \"true\" in the field   |
+              #         originate TLS (no TLSContext)      | \n If the meaning of the
+              #         representation contradicts anything else (if a TLSContext is
+              #         to be used, or in the case of \"bool:true\" if TLS is not to
+              #         be originated), then this field is ignored."
+              #       enum:
+              #       - ""
+              #       - "null"
+              #       - bool:true
+              #       - bool:false
+              #       - string
+              #       type: string
+              #   type: object
+              weight:
+                type: integer
+            required:
+            - prefix
+            - service
+            type: object
+          status:
+            description: MappingStatus defines the observed state of Mapping
+            properties:
+              reason:
+                type: string
+              state:
+                enum:
+                - ""
+                - Inactive
+                - Running
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: false
+    subresources:
+      status: {}
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.12.0
+  labels:
+    app.kubernetes.io/instance: emissary-apiext
+    app.kubernetes.io/managed-by: kubectl_apply_-f_emissary-apiext.yaml
+    app.kubernetes.io/name: emissary-apiext
+    app.kubernetes.io/part-of: emissary-apiext
+  name: tlscontexts.getambassador.io
+spec:
+  conversion:
+    strategy: Webhook
+    webhook:
+      clientConfig:
+        service:
+          name: emissary-apiext
+          namespace: emissary-system
+      conversionReviewVersions:
+      - v1
+  group: getambassador.io
+  names:
+    categories:
+    - ambassador-crds
+    kind: TLSContext
+    listKind: TLSContextList
+    plural: tlscontexts
+    singular: tlscontext
+  preserveUnknownFields: false
+  scope: Namespaced
+  versions:
+  - name: v1
+    schema:
+      openAPIV3Schema:
+        description: TLSContext is the Schema for the tlscontexts API
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: TLSContextSpec defines the desired state of TLSContext
+            properties:
+              alpn_protocols:
+                type: string
+              ca_secret:
+                type: string
+              cacert_chain_file:
+                type: string
+              cert_chain_file:
+                type: string
+              cert_required:
+                type: boolean
+              cipher_suites:
+                items:
+                  type: string
+                type: array
+              ecdh_curves:
+                items:
+                  type: string
+                type: array
+              hosts:
+                items:
+                  type: string
+                type: array
+              max_tls_version:
+                enum:
+                - v1.0
+                - v1.1
+                - v1.2
+                - v1.3
+                type: string
+              min_tls_version:
+                enum:
+                - v1.0
+                - v1.1
+                - v1.2
+                - v1.3
+                type: string
+              private_key_file:
+                type: string
+              redirect_cleartext_from:
+                type: integer
+              secret:
+                type: string
+              secret_namespacing:
+                type: boolean
+              sni:
+                type: string
+              v3CRLSecret:
+                type: string
+            type: object
+            x-kubernetes-preserve-unknown-fields: true
+        type: object
+    served: true
+    storage: false
+  - name: v2
+    schema:
+      openAPIV3Schema:
+        description: TLSContext is the Schema for the tlscontexts API
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: TLSContextSpec defines the desired state of TLSContext
+            properties:
+              alpn_protocols:
+                type: string
+              # [operator] added manually by coping over from v3alpha1
+              ambassador_id:
+                description: "AmbassadorID declares which Ambassador instances should
+                  pay attention to this resource. If no value is provided, the default
+                  is: \n ambassador_id: - \"default\""
+                items:
+                  type: string
+                type: array
+              ca_secret:
+                type: string
+              cacert_chain_file:
+                type: string
+              cert_chain_file:
+                type: string
+              cert_required:
+                type: boolean
+              cipher_suites:
+                items:
+                  type: string
+                type: array
+              ecdh_curves:
+                items:
+                  type: string
+                type: array
+              hosts:
+                items:
+                  type: string
+                type: array
+              max_tls_version:
+                enum:
+                - v1.0
+                - v1.1
+                - v1.2
+                - v1.3
+                type: string
+              min_tls_version:
+                enum:
+                - v1.0
+                - v1.1
+                - v1.2
+                - v1.3
+                type: string
+              private_key_file:
+                type: string
+              redirect_cleartext_from:
+                type: integer
+              secret:
+                type: string
+              secret_namespacing:
+                type: boolean
+              sni:
+                type: string
+              v3CRLSecret:
+                type: string
+            type: object
+            x-kubernetes-preserve-unknown-fields: true
+        type: object
+    served: true
+    storage: true
+  - name: v3alpha1
+    schema:
+      openAPIV3Schema:
+        description: TLSContext is the Schema for the tlscontexts API
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: TLSContextSpec defines the desired state of TLSContext
+            properties:
+              alpn_protocols:
+                type: string
+              ambassador_id:
+                description: "AmbassadorID declares which Ambassador instances should
+                  pay attention to this resource. If no value is provided, the default
+                  is: \n ambassador_id: - \"default\""
+                items:
+                  type: string
+                type: array
+              ca_secret:
+                type: string
+              cacert_chain_file:
+                type: string
+              cert_chain_file:
+                type: string
+              cert_required:
+                type: boolean
+              cipher_suites:
+                items:
+                  type: string
+                type: array
+              crl_secret:
+                type: string
+              ecdh_curves:
+                items:
+                  type: string
+                type: array
+              hosts:
+                items:
+                  type: string
+                type: array
+              max_tls_version:
+                enum:
+                - v1.0
+                - v1.1
+                - v1.2
+                - v1.3
+                type: string
+              min_tls_version:
+                enum:
+                - v1.0
+                - v1.1
+                - v1.2
+                - v1.3
+                type: string
+              private_key_file:
+                type: string
+              redirect_cleartext_from:
+                type: integer
+              secret:
+                type: string
+              secret_namespacing:
+                type: boolean
+              sni:
+                type: string
+            type: object
+        type: object
+    served: true
+    storage: false
diff --git a/operator/src/test/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterAmbassadorTest.java b/operator/src/test/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterAmbassadorTest.java
new file mode 100644
index 0000000..f959eab
--- /dev/null
+++ b/operator/src/test/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterAmbassadorTest.java
@@ -0,0 +1,100 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.k8s.operator.network.model.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.ReconcilerUtils;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Map;
+import java.util.stream.Stream;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+public class GerritClusterAmbassadorTest {
+
+  @ParameterizedTest
+  @MethodSource("provideYamlManifests")
+  public void expectedGerritClusterAmbassadorComponentsCreated(
+      String inputFile, Map<String, String> expectedOutputFileNames)
+      throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
+          IllegalAccessException, InvocationTargetException {
+    GerritNetwork gerritNetwork =
+        ReconcilerUtils.loadYaml(GerritNetwork.class, this.getClass(), inputFile);
+
+    for (Map.Entry<String, String> entry : expectedOutputFileNames.entrySet()) {
+      String className = entry.getKey();
+      String expectedOutputFile = entry.getValue();
+
+      Class<?> clazz = Class.forName(className);
+      Object dependentObject = clazz.getDeclaredConstructor(new Class[] {}).newInstance();
+
+      if (dependentObject instanceof MappingDependentResourceInterface) {
+        MappingDependentResourceInterface dependent =
+            (MappingDependentResourceInterface) dependentObject;
+        Mapping result = dependent.desired(gerritNetwork, null);
+        Mapping expected =
+            ReconcilerUtils.loadYaml(Mapping.class, this.getClass(), expectedOutputFile);
+        assertThat(result.getSpec()).isEqualTo(expected.getSpec());
+      }
+    }
+  }
+
+  private static Stream<Arguments> provideYamlManifests() {
+    return Stream.of(
+        Arguments.of(
+            "../../gerritnetwork_primary_replica_tls.yaml",
+            Map.of(
+                GerritClusterMappingGETReplica.class.getName(),
+                    "mappingGETReplica_primary_replica.yaml",
+                GerritClusterMappingPOSTReplica.class.getName(),
+                    "mappingPOSTReplica_primary_replica.yaml",
+                GerritClusterMappingPrimary.class.getName(), "mappingPrimary_primary_replica.yaml"
+                // TODO: add tls
+                )),
+        Arguments.of(
+            "../../gerritnetwork_primary_replica.yaml",
+            Map.of(
+                GerritClusterMappingGETReplica.class.getName(),
+                    "mappingGETReplica_primary_replica.yaml",
+                GerritClusterMappingPOSTReplica.class.getName(),
+                    "mappingPOSTReplica_primary_replica.yaml",
+                GerritClusterMappingPrimary.class.getName(),
+                    "mappingPrimary_primary_replica.yaml")),
+        Arguments.of(
+            "../../gerritnetwork_primary.yaml",
+            Map.of(GerritClusterMapping.class.getName(), "mapping_primary.yaml")),
+        Arguments.of(
+            "../../gerritnetwork_replica.yaml",
+            Map.of(GerritClusterMapping.class.getName(), "mapping_replica.yaml")),
+        Arguments.of(
+            "../../gerritnetwork_receiver_replica.yaml",
+            Map.of(
+                GerritClusterMapping.class.getName(), "mapping_replica.yaml",
+                GerritClusterMappingReceiver.class.getName(), "mapping_receiver.yaml",
+                GerritClusterMappingReceiverGET.class.getName(), "mappingGET_receiver.yaml")),
+        Arguments.of(
+            "../../gerritnetwork_receiver_replica_tls.yaml",
+            Map.of(
+                GerritClusterMapping.class.getName(), "mapping_replica.yaml",
+                GerritClusterMappingReceiver.class.getName(), "mapping_receiver.yaml",
+                GerritClusterMappingReceiverGET.class.getName(), "mappingGET_receiver.yaml"
+                // TODO: add tls
+                )));
+  }
+}
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingGETReplica_primary_replica.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingGETReplica_primary_replica.yaml
new file mode 100644
index 0000000..f2d8127
--- /dev/null
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingGETReplica_primary_replica.yaml
@@ -0,0 +1,15 @@
+apiVersion: getambassador.io/v2
+kind: Mapping
+metadata:
+  name: gerrit-mapping-get-replica
+  namespace: gerrit
+spec:
+  bypass_auth: true
+  rewrite: ""
+  host: example.com
+  method: GET
+  prefix: /.*/info/refs
+  prefix_regex: true
+  query_parameters:
+    service: git-upload-pack
+  service: replica:48080
\ No newline at end of file
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingGET_receiver.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingGET_receiver.yaml
new file mode 100644
index 0000000..c219486
--- /dev/null
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingGET_receiver.yaml
@@ -0,0 +1,15 @@
+apiVersion: getambassador.io/v2
+kind: Mapping
+metadata:
+  name: gerrit-mapping-receiver-get
+  namespace: gerrit
+spec:
+  bypass_auth: true
+  rewrite: ""
+  host: example.com
+  method: GET
+  prefix: /.*/info/refs
+  prefix_regex: true
+  query_parameters:
+    service: git-receive-pack
+  service: receiver:48081
\ No newline at end of file
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingPOSTReplica_primary_replica.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingPOSTReplica_primary_replica.yaml
new file mode 100644
index 0000000..7ebbaef
--- /dev/null
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingPOSTReplica_primary_replica.yaml
@@ -0,0 +1,13 @@
+apiVersion: getambassador.io/v2
+kind: Mapping
+metadata:
+  name: gerrit-mapping-post-replica
+  namespace: gerrit
+spec:
+  bypass_auth: true
+  rewrite: ""
+  host: example.com
+  method: POST
+  prefix: /.*/git-upload-pack
+  prefix_regex: true
+  service: replica:48080
\ No newline at end of file
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingPrimary_primary_replica.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingPrimary_primary_replica.yaml
new file mode 100644
index 0000000..e727be0
--- /dev/null
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingPrimary_primary_replica.yaml
@@ -0,0 +1,11 @@
+apiVersion: getambassador.io/v2
+kind: Mapping
+metadata:
+  name: gerrit-mapping-primary
+  namespace: gerrit
+spec:
+  bypass_auth: true
+  rewrite: ""
+  host: example.com
+  prefix: /
+  service: primary:48080
\ No newline at end of file
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mapping_primary.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mapping_primary.yaml
new file mode 100644
index 0000000..1269b1d
--- /dev/null
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mapping_primary.yaml
@@ -0,0 +1,11 @@
+apiVersion: getambassador.io/v2
+kind: Mapping
+metadata:
+  name: gerrit-mapping
+  namespace: gerrit
+spec:
+  bypass_auth: true
+  rewrite: ""
+  host: example.com
+  prefix: /
+  service: primary:48080
\ No newline at end of file
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mapping_receiver.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mapping_receiver.yaml
new file mode 100644
index 0000000..51a91cc
--- /dev/null
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mapping_receiver.yaml
@@ -0,0 +1,12 @@
+apiVersion: getambassador.io/v2
+kind: Mapping
+metadata:
+  name: gerrit-mapping-receiver
+  namespace: gerrit
+spec:
+  bypass_auth: true
+  rewrite: ""
+  host: example.com
+  prefix: /a/projects/.*|/.*/git-receive-pack
+  prefix_regex: true
+  service: receiver:48081
\ No newline at end of file
diff --git a/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mapping_replica.yaml b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mapping_replica.yaml
new file mode 100644
index 0000000..1080648
--- /dev/null
+++ b/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mapping_replica.yaml
@@ -0,0 +1,11 @@
+apiVersion: getambassador.io/v2
+kind: Mapping
+metadata:
+  name: gerrit-mapping
+  namespace: gerrit
+spec:
+  bypass_auth: true
+  rewrite: ""
+  host: example.com
+  prefix: /
+  service: replica:48080
\ No newline at end of file