update charts
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/Constants.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/Constants.java
new file mode 100644
index 0000000..d2fb405
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/Constants.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator;
+
+import com.google.inject.AbstractModule;
+
+public class Constants extends AbstractModule {
+ public static final String[] VERSIONS = new String[] {"v1alpha"};
+ public static final String[] CUSTOM_RESOURCES =
+ new String[] {"GerritCluster", "Gerrit", "Receiver", "GerritNetwork", "GitGarbageCollection"};
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/EnvModule.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/EnvModule.java
new file mode 100644
index 0000000..1f5b04c
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/EnvModule.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator;
+
+import com.google.gerrit.k8s.operator.network.IngressType;
+import com.google.inject.AbstractModule;
+import com.google.inject.name.Names;
+
+public class EnvModule extends AbstractModule {
+ @Override
+ protected void configure() {
+ bind(String.class)
+ .annotatedWith(Names.named("Namespace"))
+ .toInstance(System.getenv("NAMESPACE"));
+
+ String ingressTypeEnv = System.getenv("INGRESS");
+ IngressType ingressType =
+ ingressTypeEnv == null
+ ? IngressType.NONE
+ : IngressType.valueOf(ingressTypeEnv.toUpperCase());
+ bind(IngressType.class).annotatedWith(Names.named("IngressType")).toInstance(ingressType);
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/GerritOperator.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/GerritOperator.java
new file mode 100644
index 0000000..6f9be7e
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/GerritOperator.java
@@ -0,0 +1,111 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator;
+
+import static com.google.gerrit.k8s.operator.server.HttpServer.PORT;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+import io.fabric8.kubernetes.api.model.Service;
+import io.fabric8.kubernetes.api.model.ServiceBuilder;
+import io.fabric8.kubernetes.api.model.ServicePort;
+import io.fabric8.kubernetes.api.model.ServicePortBuilder;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import io.javaoperatorsdk.operator.Operator;
+import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
+import java.util.Map;
+import java.util.Set;
+
+@Singleton
+public class GerritOperator {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ public static final String SERVICE_NAME = "gerrit-operator";
+ public static final int SERVICE_PORT = 8080;
+
+ private final KubernetesClient client;
+ private final LifecycleManager lifecycleManager;
+
+ @SuppressWarnings("rawtypes")
+ private final Set<Reconciler> reconcilers;
+
+ private final String namespace;
+
+ private Operator operator;
+ private Service svc;
+
+ @Inject
+ @SuppressWarnings("rawtypes")
+ public GerritOperator(
+ LifecycleManager lifecycleManager,
+ KubernetesClient client,
+ Set<Reconciler> reconcilers,
+ @Named("Namespace") String namespace) {
+ this.lifecycleManager = lifecycleManager;
+ this.client = client;
+ this.reconcilers = reconcilers;
+ this.namespace = namespace;
+ }
+
+ public void start() throws Exception {
+ operator = new Operator(client);
+ for (Reconciler<?> reconciler : reconcilers) {
+ logger.atInfo().log(
+ String.format("Registering reconciler: %s", reconciler.getClass().getSimpleName()));
+ operator.register(reconciler);
+ }
+ operator.start();
+ lifecycleManager.addShutdownHook(
+ new Runnable() {
+ @Override
+ public void run() {
+ shutdown();
+ }
+ });
+ applyService();
+ }
+
+ public void shutdown() {
+ client.resource(svc).delete();
+ operator.stop();
+ }
+
+ private void applyService() {
+ ServicePort port =
+ new ServicePortBuilder()
+ .withName("http")
+ .withPort(SERVICE_PORT)
+ .withNewTargetPort(PORT)
+ .withProtocol("TCP")
+ .build();
+ svc =
+ new ServiceBuilder()
+ .withApiVersion("v1")
+ .withNewMetadata()
+ .withName(SERVICE_NAME)
+ .withNamespace(namespace)
+ .endMetadata()
+ .withNewSpec()
+ .withType("ClusterIP")
+ .withPorts(port)
+ .withSelector(Map.of("app", "gerrit-operator"))
+ .endSpec()
+ .build();
+
+ logger.atInfo().log(String.format("Applying Service for Gerrit Operator: %s", svc.toString()));
+ client.resource(svc).createOrReplace();
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/LifecycleManager.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/LifecycleManager.java
new file mode 100644
index 0000000..8e556a7
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/LifecycleManager.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator;
+
+import com.google.common.collect.Lists;
+import com.google.inject.Singleton;
+import java.util.ArrayList;
+import java.util.List;
+
+@Singleton
+public class LifecycleManager {
+ private List<Runnable> shutdownHooks = new ArrayList<>();
+
+ public LifecycleManager() {
+ Runtime.getRuntime().addShutdownHook(new Thread(this::executeShutdownHooks));
+ }
+
+ public void addShutdownHook(Runnable hook) {
+ shutdownHooks.add(hook);
+ }
+
+ private void executeShutdownHooks() {
+ for (Runnable hook : Lists.reverse(shutdownHooks)) {
+ hook.run();
+ }
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/Main.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/Main.java
new file mode 100644
index 0000000..8fc1428
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/Main.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator;
+
+import com.google.gerrit.k8s.operator.admission.ValidationWebhookConfigs;
+import com.google.gerrit.k8s.operator.server.HttpServer;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Stage;
+
+public class Main {
+
+ public static void main(String[] args) throws Exception {
+ Injector injector = Guice.createInjector(Stage.PRODUCTION, new OperatorModule());
+ injector.getInstance(HttpServer.class).start();
+ injector.getInstance(ValidationWebhookConfigs.class).apply();
+ injector.getInstance(GerritOperator.class).start();
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/OperatorModule.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/OperatorModule.java
new file mode 100644
index 0000000..3e61528
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/OperatorModule.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator;
+
+import com.google.gerrit.k8s.operator.admission.AdmissionWebhookModule;
+import com.google.gerrit.k8s.operator.cluster.GerritClusterReconciler;
+import com.google.gerrit.k8s.operator.gerrit.GerritReconciler;
+import com.google.gerrit.k8s.operator.gitgc.GitGarbageCollectionReconciler;
+import com.google.gerrit.k8s.operator.network.GerritNetworkReconcilerProvider;
+import com.google.gerrit.k8s.operator.receiver.ReceiverReconciler;
+import com.google.gerrit.k8s.operator.server.ServerModule;
+import com.google.inject.AbstractModule;
+import com.google.inject.multibindings.Multibinder;
+import io.fabric8.kubernetes.client.Config;
+import io.fabric8.kubernetes.client.ConfigBuilder;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import io.fabric8.kubernetes.client.KubernetesClientBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
+
+public class OperatorModule extends AbstractModule {
+ @SuppressWarnings("rawtypes")
+ @Override
+ protected void configure() {
+ install(new EnvModule());
+ install(new ServerModule());
+
+ bind(KubernetesClient.class).toInstance(getKubernetesClient());
+ bind(LifecycleManager.class);
+ bind(GerritOperator.class);
+
+ install(new AdmissionWebhookModule());
+
+ Multibinder<Reconciler> reconcilers = Multibinder.newSetBinder(binder(), Reconciler.class);
+ reconcilers.addBinding().to(GerritClusterReconciler.class);
+ reconcilers.addBinding().to(GerritReconciler.class);
+ reconcilers.addBinding().to(GitGarbageCollectionReconciler.class);
+ reconcilers.addBinding().to(ReceiverReconciler.class);
+ reconcilers.addBinding().toProvider(GerritNetworkReconcilerProvider.class);
+ }
+
+ private KubernetesClient getKubernetesClient() {
+ Config config = new ConfigBuilder().withNamespace(null).build();
+ return new KubernetesClientBuilder().withConfig(config).build();
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/admission/AdmissionWebhookModule.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/admission/AdmissionWebhookModule.java
new file mode 100644
index 0000000..d3d4841
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/admission/AdmissionWebhookModule.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.admission;
+
+import com.google.gerrit.k8s.operator.v1alpha.admission.GerritClusterValidationWebhookConfigApplier;
+import com.google.gerrit.k8s.operator.v1alpha.admission.GerritValidationWebhookConfigApplier;
+import com.google.gerrit.k8s.operator.v1alpha.admission.GitGcValidationWebhookConfigApplier;
+import com.google.inject.AbstractModule;
+import com.google.inject.assistedinject.FactoryModuleBuilder;
+
+public class AdmissionWebhookModule extends AbstractModule {
+ public void configure() {
+ install(new FactoryModuleBuilder().build(ValidationWebhookConfigApplier.Factory.class));
+
+ bind(ValidationWebhookConfigs.class);
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/admission/ValidationWebhookConfigApplier.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/admission/ValidationWebhookConfigApplier.java
new file mode 100644
index 0000000..443347e
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/admission/ValidationWebhookConfigApplier.java
@@ -0,0 +1,144 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.admission;
+
+import static com.google.gerrit.k8s.operator.GerritOperator.SERVICE_NAME;
+import static com.google.gerrit.k8s.operator.GerritOperator.SERVICE_PORT;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.k8s.operator.server.KeyStoreProvider;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+import com.google.inject.name.Named;
+import io.fabric8.kubernetes.api.model.admissionregistration.v1.RuleWithOperations;
+import io.fabric8.kubernetes.api.model.admissionregistration.v1.RuleWithOperationsBuilder;
+import io.fabric8.kubernetes.api.model.admissionregistration.v1.ValidatingWebhook;
+import io.fabric8.kubernetes.api.model.admissionregistration.v1.ValidatingWebhookBuilder;
+import io.fabric8.kubernetes.api.model.admissionregistration.v1.ValidatingWebhookConfiguration;
+import io.fabric8.kubernetes.api.model.admissionregistration.v1.ValidatingWebhookConfigurationBuilder;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import java.io.IOException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.List;
+
+public class ValidationWebhookConfigApplier {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final KubernetesClient client;
+ private final String namespace;
+ private final KeyStoreProvider keyStoreProvider;
+ private final ValidatingWebhookConfiguration cfg;
+ private final String customResourceName;
+ private final String[] customResourceVersions;
+
+ public interface Factory {
+ ValidationWebhookConfigApplier create(
+ String customResourceName, String[] customResourceVersions);
+ }
+
+ @AssistedInject
+ ValidationWebhookConfigApplier(
+ KubernetesClient client,
+ @Named("Namespace") String namespace,
+ KeyStoreProvider keyStoreProvider,
+ @Assisted String customResourceName,
+ @Assisted String[] customResourceVersions) {
+ this.client = client;
+ this.namespace = namespace;
+ this.keyStoreProvider = keyStoreProvider;
+ this.customResourceName = customResourceName;
+ this.customResourceVersions = customResourceVersions;
+
+ this.cfg = build();
+ }
+
+ public List<RuleWithOperations> rules(String version) {
+ return List.of(
+ new RuleWithOperationsBuilder()
+ .withApiGroups("gerritoperator.google.com")
+ .withApiVersions(version)
+ .withOperations("CREATE", "UPDATE")
+ .withResources(customResourceName)
+ .withScope("*")
+ .build());
+ }
+
+ public List<ValidatingWebhook> webhooks()
+ throws CertificateEncodingException, KeyStoreException, NoSuchAlgorithmException,
+ CertificateException, IOException {
+ List<ValidatingWebhook> webhooks = new ArrayList<>();
+ for (String version : customResourceVersions) {
+ webhooks.add(
+ new ValidatingWebhookBuilder()
+ .withName(customResourceName.toLowerCase() + "." + version + ".validator.google.com")
+ .withAdmissionReviewVersions("v1", "v1beta1")
+ .withNewClientConfig()
+ .withCaBundle(caBundle())
+ .withNewService()
+ .withName(SERVICE_NAME)
+ .withNamespace(namespace)
+ .withPath(
+ String.format("/admission/%s/%s", version, customResourceName).toLowerCase())
+ .withPort(SERVICE_PORT)
+ .endService()
+ .endClientConfig()
+ .withFailurePolicy("Fail")
+ .withMatchPolicy("Equivalent")
+ .withRules(rules(version))
+ .withTimeoutSeconds(10)
+ .withSideEffects("None")
+ .build());
+ }
+ return webhooks;
+ }
+
+ private String caBundle()
+ throws CertificateEncodingException, KeyStoreException, NoSuchAlgorithmException,
+ CertificateException, IOException {
+ return Base64.getEncoder().encodeToString(keyStoreProvider.getCertificate().getBytes());
+ }
+
+ public ValidatingWebhookConfiguration build() {
+ try {
+ return new ValidatingWebhookConfigurationBuilder()
+ .withNewMetadata()
+ .withName(customResourceName.toLowerCase())
+ .endMetadata()
+ .withWebhooks(webhooks())
+ .build();
+ } catch (CertificateException | IOException | KeyStoreException | NoSuchAlgorithmException e) {
+ throw new RuntimeException(
+ "Failed to deploy ValidationWebhookConfiguration " + customResourceName, e);
+ }
+ }
+
+ public void apply()
+ throws KeyStoreException, NoSuchProviderException, IOException, NoSuchAlgorithmException,
+ CertificateException {
+ logger.atInfo().log("Applying webhook config %s", cfg);
+ client.resource(cfg).createOrReplace();
+ }
+
+ public void delete() {
+ logger.atInfo().log("Deleting webhook config %s", cfg);
+ client.resource(cfg).delete();
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/admission/ValidationWebhookConfigs.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/admission/ValidationWebhookConfigs.java
new file mode 100644
index 0000000..901d15f
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/admission/ValidationWebhookConfigs.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.admission;
+
+import static com.google.gerrit.k8s.operator.Constants.CUSTOM_RESOURCES;
+import static com.google.gerrit.k8s.operator.Constants.VERSIONS;
+
+import com.google.gerrit.k8s.operator.LifecycleManager;
+import com.google.inject.Inject;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ValidationWebhookConfigs {
+
+ private final List<ValidationWebhookConfigApplier> configAppliers;
+
+ @Inject
+ public ValidationWebhookConfigs(
+ LifecycleManager lifecycleManager,
+ ValidationWebhookConfigApplier.Factory configApplierFactory) {
+ this.configAppliers = new ArrayList<>();
+
+ for (String customResourceName : CUSTOM_RESOURCES) {
+ this.configAppliers.add(configApplierFactory.create(customResourceName, VERSIONS));
+ }
+
+ lifecycleManager.addShutdownHook(
+ new Runnable() {
+
+ @Override
+ public void run() {
+ delete();
+ }
+ });
+ }
+
+ public void apply() throws Exception {
+ for (ValidationWebhookConfigApplier applier : configAppliers) {
+ applier.apply();
+ }
+ }
+
+ public void delete() {
+ for (ValidationWebhookConfigApplier applier : configAppliers) {
+ applier.delete();
+ }
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/GerritClusterReconciler.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/GerritClusterReconciler.java
new file mode 100644
index 0000000..c00d51e
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/GerritClusterReconciler.java
@@ -0,0 +1,149 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.cluster;
+
+import static com.google.gerrit.k8s.operator.cluster.GerritClusterReconciler.CLUSTER_MANAGED_GERRIT_EVENT_SOURCE;
+import static com.google.gerrit.k8s.operator.cluster.GerritClusterReconciler.CLUSTER_MANAGED_GERRIT_NETWORK_EVENT_SOURCE;
+import static com.google.gerrit.k8s.operator.cluster.GerritClusterReconciler.CLUSTER_MANAGED_RECEIVER_EVENT_SOURCE;
+import static com.google.gerrit.k8s.operator.cluster.GerritClusterReconciler.CM_EVENT_SOURCE;
+import static com.google.gerrit.k8s.operator.cluster.GerritClusterReconciler.PVC_EVENT_SOURCE;
+
+import com.google.gerrit.k8s.operator.cluster.dependent.ClusterManagedGerrit;
+import com.google.gerrit.k8s.operator.cluster.dependent.ClusterManagedGerritCondition;
+import com.google.gerrit.k8s.operator.cluster.dependent.ClusterManagedGerritNetwork;
+import com.google.gerrit.k8s.operator.cluster.dependent.ClusterManagedGerritNetworkCondition;
+import com.google.gerrit.k8s.operator.cluster.dependent.ClusterManagedReceiver;
+import com.google.gerrit.k8s.operator.cluster.dependent.ClusterManagedReceiverCondition;
+import com.google.gerrit.k8s.operator.cluster.dependent.NfsIdmapdConfigMap;
+import com.google.gerrit.k8s.operator.cluster.dependent.NfsWorkaroundCondition;
+import com.google.gerrit.k8s.operator.cluster.dependent.SharedPVC;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritClusterStatus;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplate;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.Receiver;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.ReceiverTemplate;
+import com.google.inject.Singleton;
+import io.fabric8.kubernetes.api.model.ConfigMap;
+import io.fabric8.kubernetes.api.model.PersistentVolumeClaim;
+import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
+import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer;
+import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
+import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
+import io.javaoperatorsdk.operator.processing.event.source.EventSource;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Singleton
+@ControllerConfiguration(
+ dependents = {
+ @Dependent(
+ name = "shared-pvc",
+ type = SharedPVC.class,
+ useEventSourceWithName = PVC_EVENT_SOURCE),
+ @Dependent(
+ type = NfsIdmapdConfigMap.class,
+ reconcilePrecondition = NfsWorkaroundCondition.class,
+ useEventSourceWithName = CM_EVENT_SOURCE),
+ @Dependent(
+ name = "gerrits",
+ type = ClusterManagedGerrit.class,
+ reconcilePrecondition = ClusterManagedGerritCondition.class,
+ useEventSourceWithName = CLUSTER_MANAGED_GERRIT_EVENT_SOURCE),
+ @Dependent(
+ name = "receiver",
+ type = ClusterManagedReceiver.class,
+ reconcilePrecondition = ClusterManagedReceiverCondition.class,
+ useEventSourceWithName = CLUSTER_MANAGED_RECEIVER_EVENT_SOURCE),
+ @Dependent(
+ type = ClusterManagedGerritNetwork.class,
+ reconcilePrecondition = ClusterManagedGerritNetworkCondition.class,
+ useEventSourceWithName = CLUSTER_MANAGED_GERRIT_NETWORK_EVENT_SOURCE),
+ })
+public class GerritClusterReconciler
+ implements Reconciler<GerritCluster>, EventSourceInitializer<GerritCluster> {
+ public static final String CM_EVENT_SOURCE = "cm-event-source";
+ public static final String PVC_EVENT_SOURCE = "pvc-event-source";
+ public static final String CLUSTER_MANAGED_GERRIT_EVENT_SOURCE = "cluster-managed-gerrit";
+ public static final String CLUSTER_MANAGED_RECEIVER_EVENT_SOURCE = "cluster-managed-receiver";
+ public static final String CLUSTER_MANAGED_GERRIT_NETWORK_EVENT_SOURCE =
+ "cluster-managed-gerrit-network";
+
+ @Override
+ public Map<String, EventSource> prepareEventSources(EventSourceContext<GerritCluster> context) {
+ InformerEventSource<ConfigMap, GerritCluster> cmEventSource =
+ new InformerEventSource<>(
+ InformerConfiguration.from(ConfigMap.class, context).build(), context);
+
+ InformerEventSource<PersistentVolumeClaim, GerritCluster> pvcEventSource =
+ new InformerEventSource<>(
+ InformerConfiguration.from(PersistentVolumeClaim.class, context).build(), context);
+
+ InformerEventSource<Gerrit, GerritCluster> clusterManagedGerritEventSource =
+ new InformerEventSource<>(
+ InformerConfiguration.from(Gerrit.class, context).build(), context);
+
+ InformerEventSource<Receiver, GerritCluster> clusterManagedReceiverEventSource =
+ new InformerEventSource<>(
+ InformerConfiguration.from(Receiver.class, context).build(), context);
+
+ InformerEventSource<GerritNetwork, GerritCluster> clusterManagedGerritNetworkEventSource =
+ new InformerEventSource<>(
+ InformerConfiguration.from(GerritNetwork.class, context).build(), context);
+
+ Map<String, EventSource> eventSources = new HashMap<>();
+ eventSources.put(CM_EVENT_SOURCE, cmEventSource);
+ eventSources.put(PVC_EVENT_SOURCE, pvcEventSource);
+ eventSources.put(CLUSTER_MANAGED_GERRIT_EVENT_SOURCE, clusterManagedGerritEventSource);
+ eventSources.put(CLUSTER_MANAGED_RECEIVER_EVENT_SOURCE, clusterManagedReceiverEventSource);
+ eventSources.put(
+ CLUSTER_MANAGED_GERRIT_NETWORK_EVENT_SOURCE, clusterManagedGerritNetworkEventSource);
+ return eventSources;
+ }
+
+ @Override
+ public UpdateControl<GerritCluster> reconcile(
+ GerritCluster gerritCluster, Context<GerritCluster> context) {
+ List<GerritTemplate> managedGerrits = gerritCluster.getSpec().getGerrits();
+ Map<String, List<String>> members = new HashMap<>();
+ members.put(
+ "gerrit",
+ managedGerrits.stream().map(g -> g.getMetadata().getName()).collect(Collectors.toList()));
+ ReceiverTemplate managedReceiver = gerritCluster.getSpec().getReceiver();
+ if (managedReceiver != null) {
+ members.put("receiver", List.of(managedReceiver.getMetadata().getName()));
+ }
+ return UpdateControl.patchStatus(updateStatus(gerritCluster, members));
+ }
+
+ private GerritCluster updateStatus(
+ GerritCluster gerritCluster, Map<String, List<String>> members) {
+ GerritClusterStatus status = gerritCluster.getStatus();
+ if (status == null) {
+ status = new GerritClusterStatus();
+ }
+ status.setMembers(members);
+ gerritCluster.setStatus(status);
+ return gerritCluster;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerrit.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerrit.java
new file mode 100644
index 0000000..8de0b3e
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerrit.java
@@ -0,0 +1,66 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.cluster.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplate;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected;
+import io.javaoperatorsdk.operator.processing.dependent.BulkDependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.Creator;
+import io.javaoperatorsdk.operator.processing.dependent.Updater;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+public class ClusterManagedGerrit extends KubernetesDependentResource<Gerrit, GerritCluster>
+ implements Creator<Gerrit, GerritCluster>,
+ Updater<Gerrit, GerritCluster>,
+ Deleter<GerritCluster>,
+ BulkDependentResource<Gerrit, GerritCluster>,
+ GarbageCollected<GerritCluster> {
+
+ public ClusterManagedGerrit() {
+ super(Gerrit.class);
+ }
+
+ @Override
+ public Map<String, Gerrit> desiredResources(
+ GerritCluster gerritCluster, Context<GerritCluster> context) {
+ Map<String, Gerrit> gerrits = new HashMap<>();
+ for (GerritTemplate template : gerritCluster.getSpec().getGerrits()) {
+ gerrits.put(template.getMetadata().getName(), desired(gerritCluster, template));
+ }
+ return gerrits;
+ }
+
+ private Gerrit desired(GerritCluster gerritCluster, GerritTemplate template) {
+ return template.toGerrit(gerritCluster);
+ }
+
+ @Override
+ public Map<String, Gerrit> getSecondaryResources(
+ GerritCluster primary, Context<GerritCluster> context) {
+ Set<Gerrit> gerrits = context.getSecondaryResources(Gerrit.class);
+ Map<String, Gerrit> result = new HashMap<>(gerrits.size());
+ for (Gerrit gerrit : gerrits) {
+ result.put(gerrit.getMetadata().getName(), gerrit);
+ }
+ return result;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerritCondition.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerritCondition.java
new file mode 100644
index 0000000..7455aca
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerritCondition.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.cluster.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
+
+public class ClusterManagedGerritCondition implements Condition<Gerrit, GerritCluster> {
+
+ @Override
+ public boolean isMet(
+ DependentResource<Gerrit, GerritCluster> dependentResource,
+ GerritCluster gerritCluster,
+ Context<GerritCluster> context) {
+ return !gerritCluster.getSpec().getGerrits().isEmpty();
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerritNetwork.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerritNetwork.java
new file mode 100644
index 0000000..232c791
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerritNetwork.java
@@ -0,0 +1,81 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.cluster.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplate;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplateSpec.GerritMode;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetworkSpec;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.NetworkMember;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.NetworkMemberWithSsh;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.ReceiverTemplate;
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+import java.util.Optional;
+
+@KubernetesDependent
+public class ClusterManagedGerritNetwork
+ extends CRUDKubernetesDependentResource<GerritNetwork, GerritCluster> {
+ public static final String NAME_SUFFIX = "gerrit-network";
+
+ public ClusterManagedGerritNetwork() {
+ super(GerritNetwork.class);
+ }
+
+ @Override
+ public GerritNetwork desired(GerritCluster gerritCluster, Context<GerritCluster> context) {
+ GerritNetwork gerritNetwork = new GerritNetwork();
+ gerritNetwork.setMetadata(
+ new ObjectMetaBuilder()
+ .withName(gerritCluster.getDependentResourceName(NAME_SUFFIX))
+ .withNamespace(gerritCluster.getMetadata().getNamespace())
+ .build());
+ GerritNetworkSpec gerritNetworkSpec = new GerritNetworkSpec();
+
+ Optional<GerritTemplate> optionalPrimaryGerrit =
+ gerritCluster.getSpec().getGerrits().stream()
+ .filter(g -> g.getSpec().getMode().equals(GerritMode.PRIMARY))
+ .findFirst();
+ if (optionalPrimaryGerrit.isPresent()) {
+ GerritTemplate primaryGerrit = optionalPrimaryGerrit.get();
+ gerritNetworkSpec.setPrimaryGerrit(
+ new NetworkMemberWithSsh(
+ primaryGerrit.getMetadata().getName(), primaryGerrit.getSpec().getService()));
+ }
+
+ Optional<GerritTemplate> optionalGerritReplica =
+ gerritCluster.getSpec().getGerrits().stream()
+ .filter(g -> g.getSpec().getMode().equals(GerritMode.REPLICA))
+ .findFirst();
+ if (optionalGerritReplica.isPresent()) {
+ GerritTemplate gerritReplica = optionalGerritReplica.get();
+ gerritNetworkSpec.setGerritReplica(
+ new NetworkMemberWithSsh(
+ gerritReplica.getMetadata().getName(), gerritReplica.getSpec().getService()));
+ }
+
+ ReceiverTemplate receiver = gerritCluster.getSpec().getReceiver();
+ if (receiver != null) {
+ gerritNetworkSpec.setReceiver(
+ new NetworkMember(receiver.getMetadata().getName(), receiver.getSpec().getService()));
+ }
+ gerritNetworkSpec.setIngress(gerritCluster.getSpec().getIngress());
+ gerritNetwork.setSpec(gerritNetworkSpec);
+ return gerritNetwork;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerritNetworkCondition.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerritNetworkCondition.java
new file mode 100644
index 0000000..a5b9244
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedGerritNetworkCondition.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.cluster.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
+
+public class ClusterManagedGerritNetworkCondition
+ implements Condition<GerritNetwork, GerritCluster> {
+
+ @Override
+ public boolean isMet(
+ DependentResource<GerritNetwork, GerritCluster> dependentResource,
+ GerritCluster gerritCluster,
+ Context<GerritCluster> context) {
+ return gerritCluster.getSpec().getIngress().isEnabled();
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedReceiver.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedReceiver.java
new file mode 100644
index 0000000..62618a2
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedReceiver.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.cluster.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.Receiver;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
+
+public class ClusterManagedReceiver
+ extends CRUDKubernetesDependentResource<Receiver, GerritCluster> {
+
+ public ClusterManagedReceiver() {
+ super(Receiver.class);
+ }
+
+ @Override
+ public Receiver desired(GerritCluster gerritCluster, Context<GerritCluster> context) {
+ return gerritCluster.getSpec().getReceiver().toReceiver(gerritCluster);
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedReceiverCondition.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedReceiverCondition.java
new file mode 100644
index 0000000..aa27446
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/ClusterManagedReceiverCondition.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.cluster.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.Receiver;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
+
+public class ClusterManagedReceiverCondition implements Condition<Receiver, GerritCluster> {
+
+ @Override
+ public boolean isMet(
+ DependentResource<Receiver, GerritCluster> dependentResource,
+ GerritCluster gerritCluster,
+ Context<GerritCluster> context) {
+ return gerritCluster.getSpec().getReceiver() != null;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/NfsIdmapdConfigMap.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/NfsIdmapdConfigMap.java
new file mode 100644
index 0000000..623e801
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/NfsIdmapdConfigMap.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.cluster.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import io.fabric8.kubernetes.api.model.ConfigMap;
+import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+import java.util.Map;
+
+@KubernetesDependent(resourceDiscriminator = NfsIdmapdConfigMapDiscriminator.class)
+public class NfsIdmapdConfigMap extends CRUDKubernetesDependentResource<ConfigMap, GerritCluster> {
+ public static final String NFS_IDMAPD_CM_NAME = "nfs-idmapd-config";
+
+ public NfsIdmapdConfigMap() {
+ super(ConfigMap.class);
+ }
+
+ @Override
+ protected ConfigMap desired(GerritCluster gerritCluster, Context<GerritCluster> context) {
+ return new ConfigMapBuilder()
+ .withNewMetadata()
+ .withName(NFS_IDMAPD_CM_NAME)
+ .withNamespace(gerritCluster.getMetadata().getNamespace())
+ .withLabels(gerritCluster.getLabels(NFS_IDMAPD_CM_NAME, this.getClass().getSimpleName()))
+ .endMetadata()
+ .withData(
+ Map.of(
+ "idmapd.conf",
+ gerritCluster
+ .getSpec()
+ .getStorage()
+ .getStorageClasses()
+ .getNfsWorkaround()
+ .getIdmapdConfig()))
+ .build();
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/NfsIdmapdConfigMapDiscriminator.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/NfsIdmapdConfigMapDiscriminator.java
new file mode 100644
index 0000000..17dfea7
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/NfsIdmapdConfigMapDiscriminator.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.cluster.dependent;
+
+import static com.google.gerrit.k8s.operator.cluster.GerritClusterReconciler.CM_EVENT_SOURCE;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import io.fabric8.kubernetes.api.model.ConfigMap;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.Optional;
+
+public class NfsIdmapdConfigMapDiscriminator
+ implements ResourceDiscriminator<ConfigMap, GerritCluster> {
+ @Override
+ public Optional<ConfigMap> distinguish(
+ Class<ConfigMap> resource, GerritCluster primary, Context<GerritCluster> context) {
+ InformerEventSource<ConfigMap, GerritCluster> ies =
+ (InformerEventSource<ConfigMap, GerritCluster>)
+ context
+ .eventSourceRetriever()
+ .getResourceEventSourceFor(ConfigMap.class, CM_EVENT_SOURCE);
+
+ return ies.get(
+ new ResourceID(
+ NfsIdmapdConfigMap.NFS_IDMAPD_CM_NAME, primary.getMetadata().getNamespace()));
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/NfsWorkaroundCondition.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/NfsWorkaroundCondition.java
new file mode 100644
index 0000000..a0cccc0
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/NfsWorkaroundCondition.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.cluster.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.NfsWorkaroundConfig;
+import io.fabric8.kubernetes.api.model.ConfigMap;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
+
+public class NfsWorkaroundCondition implements Condition<ConfigMap, GerritCluster> {
+ @Override
+ public boolean isMet(
+ DependentResource<ConfigMap, GerritCluster> dependentResource,
+ GerritCluster gerritCluster,
+ Context<GerritCluster> context) {
+ NfsWorkaroundConfig cfg =
+ gerritCluster.getSpec().getStorage().getStorageClasses().getNfsWorkaround();
+ return cfg.isEnabled() && cfg.getIdmapdConfig() != null;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/SharedPVC.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/SharedPVC.java
new file mode 100644
index 0000000..099afc6
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/SharedPVC.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.cluster.dependent;
+
+import com.google.gerrit.k8s.operator.util.CRUDKubernetesDependentPVCResource;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GerritStorageConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.SharedStorage;
+import io.fabric8.kubernetes.api.model.PersistentVolumeClaim;
+import io.fabric8.kubernetes.api.model.PersistentVolumeClaimBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+import java.util.Map;
+
+@KubernetesDependent(resourceDiscriminator = SharedPVCDiscriminator.class)
+public class SharedPVC extends CRUDKubernetesDependentPVCResource<GerritCluster> {
+
+ public static final String SHARED_PVC_NAME = "shared-pvc";
+
+ @Override
+ protected PersistentVolumeClaim desiredPVC(
+ GerritCluster gerritCluster, Context<GerritCluster> context) {
+ GerritStorageConfig storageConfig = gerritCluster.getSpec().getStorage();
+ SharedStorage sharedStorage = storageConfig.getSharedStorage();
+ return new PersistentVolumeClaimBuilder()
+ .withNewMetadata()
+ .withName(SHARED_PVC_NAME)
+ .withNamespace(gerritCluster.getMetadata().getNamespace())
+ .withLabels(gerritCluster.getLabels("shared-storage", this.getClass().getSimpleName()))
+ .endMetadata()
+ .withNewSpec()
+ .withAccessModes("ReadWriteMany")
+ .withNewResources()
+ .withRequests(Map.of("storage", sharedStorage.getSize()))
+ .endResources()
+ .withStorageClassName(storageConfig.getStorageClasses().getReadWriteMany())
+ .withSelector(sharedStorage.getSelector())
+ .withVolumeName(sharedStorage.getVolumeName())
+ .endSpec()
+ .build();
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/SharedPVCDiscriminator.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/SharedPVCDiscriminator.java
new file mode 100644
index 0000000..52fe941
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/SharedPVCDiscriminator.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.cluster.dependent;
+
+import static com.google.gerrit.k8s.operator.cluster.GerritClusterReconciler.PVC_EVENT_SOURCE;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import io.fabric8.kubernetes.api.model.PersistentVolumeClaim;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.Optional;
+
+public class SharedPVCDiscriminator
+ implements ResourceDiscriminator<PersistentVolumeClaim, GerritCluster> {
+ @Override
+ public Optional<PersistentVolumeClaim> distinguish(
+ Class<PersistentVolumeClaim> resource,
+ GerritCluster primary,
+ Context<GerritCluster> context) {
+ InformerEventSource<PersistentVolumeClaim, GerritCluster> ies =
+ (InformerEventSource<PersistentVolumeClaim, GerritCluster>)
+ context
+ .eventSourceRetriever()
+ .getResourceEventSourceFor(PersistentVolumeClaim.class, PVC_EVENT_SOURCE);
+
+ return ies.get(new ResourceID(SharedPVC.SHARED_PVC_NAME, primary.getMetadata().getNamespace()));
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/GerritReconciler.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/GerritReconciler.java
new file mode 100644
index 0000000..04d0a2f
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/GerritReconciler.java
@@ -0,0 +1,146 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gerrit;
+
+import static com.google.gerrit.k8s.operator.gerrit.GerritReconciler.CONFIG_MAP_EVENT_SOURCE;
+import static com.google.gerrit.k8s.operator.gerrit.dependent.GerritSecret.CONTEXT_SECRET_VERSION_KEY;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.k8s.operator.gerrit.dependent.GerritConfigMap;
+import com.google.gerrit.k8s.operator.gerrit.dependent.GerritInitConfigMap;
+import com.google.gerrit.k8s.operator.gerrit.dependent.GerritSecret;
+import com.google.gerrit.k8s.operator.gerrit.dependent.GerritService;
+import com.google.gerrit.k8s.operator.gerrit.dependent.GerritStatefulSet;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritStatus;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import io.fabric8.kubernetes.api.model.ConfigMap;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
+import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer;
+import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
+import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
+import io.javaoperatorsdk.operator.processing.dependent.workflow.WorkflowReconcileResult;
+import io.javaoperatorsdk.operator.processing.event.source.EventSource;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+@Singleton
+@ControllerConfiguration(
+ dependents = {
+ @Dependent(name = "gerrit-secret", type = GerritSecret.class),
+ @Dependent(
+ name = "gerrit-configmap",
+ type = GerritConfigMap.class,
+ useEventSourceWithName = CONFIG_MAP_EVENT_SOURCE),
+ @Dependent(
+ name = "gerrit-init-configmap",
+ type = GerritInitConfigMap.class,
+ useEventSourceWithName = CONFIG_MAP_EVENT_SOURCE),
+ @Dependent(
+ name = "gerrit-statefulset",
+ type = GerritStatefulSet.class,
+ dependsOn = {"gerrit-configmap", "gerrit-init-configmap"}),
+ @Dependent(
+ name = "gerrit-service",
+ type = GerritService.class,
+ dependsOn = {"gerrit-statefulset"})
+ })
+public class GerritReconciler implements Reconciler<Gerrit>, EventSourceInitializer<Gerrit> {
+ public static final String CONFIG_MAP_EVENT_SOURCE = "configmap-event-source";
+
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final KubernetesClient client;
+
+ @Inject
+ public GerritReconciler(KubernetesClient client) {
+ this.client = client;
+ }
+
+ @Override
+ public Map<String, EventSource> prepareEventSources(EventSourceContext<Gerrit> context) {
+ InformerEventSource<ConfigMap, Gerrit> configmapEventSource =
+ new InformerEventSource<>(
+ InformerConfiguration.from(ConfigMap.class, context).build(), context);
+
+ Map<String, EventSource> eventSources = new HashMap<>();
+ eventSources.put(CONFIG_MAP_EVENT_SOURCE, configmapEventSource);
+ return eventSources;
+ }
+
+ @Override
+ public UpdateControl<Gerrit> reconcile(Gerrit gerrit, Context<Gerrit> context) throws Exception {
+ return UpdateControl.patchStatus(updateStatus(gerrit, context));
+ }
+
+ private Gerrit updateStatus(Gerrit gerrit, Context<Gerrit> context) {
+ GerritStatus status = gerrit.getStatus();
+ if (status == null) {
+ status = new GerritStatus();
+ }
+ Optional<WorkflowReconcileResult> result =
+ context.managedDependentResourceContext().getWorkflowReconcileResult();
+ if (result.isPresent()) {
+ status.setReady(result.get().allDependentResourcesReady());
+ } else {
+ status.setReady(false);
+ }
+
+ Map<String, String> cmVersions = new HashMap<>();
+
+ cmVersions.put(
+ GerritConfigMap.getName(gerrit),
+ client
+ .configMaps()
+ .inNamespace(gerrit.getMetadata().getNamespace())
+ .withName(GerritConfigMap.getName(gerrit))
+ .get()
+ .getMetadata()
+ .getResourceVersion());
+
+ cmVersions.put(
+ GerritInitConfigMap.getName(gerrit),
+ client
+ .configMaps()
+ .inNamespace(gerrit.getMetadata().getNamespace())
+ .withName(GerritInitConfigMap.getName(gerrit))
+ .get()
+ .getMetadata()
+ .getResourceVersion());
+
+ logger.atFine().log("Adding ConfigMap versions: %s", cmVersions);
+ status.setAppliedConfigMapVersions(cmVersions);
+
+ Map<String, String> secretVersions = new HashMap<>();
+ Optional<String> gerritSecret =
+ context.managedDependentResourceContext().get(CONTEXT_SECRET_VERSION_KEY, String.class);
+ if (gerritSecret.isPresent()) {
+ secretVersions.put(gerrit.getSpec().getSecretRef(), gerritSecret.get());
+ }
+ logger.atFine().log("Adding Secret versions: %s", secretVersions);
+ status.setAppliedSecretVersions(secretVersions);
+
+ gerrit.setStatus(status);
+ return gerrit;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/ConfigBuilder.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/ConfigBuilder.java
new file mode 100644
index 0000000..ca58e94
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/ConfigBuilder.java
@@ -0,0 +1,91 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gerrit.config;
+
+import com.google.common.collect.ImmutableList;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+
+public abstract class ConfigBuilder {
+
+ private final ImmutableList<RequiredOption<?>> requiredOptions;
+ private final Config config;
+
+ ConfigBuilder(Config baseConfig, ImmutableList<RequiredOption<?>> requiredOptions) {
+ this.config = baseConfig;
+ this.requiredOptions = requiredOptions;
+ }
+
+ protected ConfigBuilder(String baseConfig, ImmutableList<RequiredOption<?>> requiredOptions) {
+ this.config = parseConfig(baseConfig);
+ this.requiredOptions = requiredOptions;
+ }
+
+ public Config build() {
+ ConfigValidator configValidator = new ConfigValidator(requiredOptions);
+ try {
+ configValidator.check(config);
+ } catch (InvalidGerritConfigException e) {
+ throw new IllegalStateException(e);
+ }
+ setRequiredOptions();
+ return config;
+ }
+
+ public void validate() throws InvalidGerritConfigException {
+ new ConfigValidator(requiredOptions).check(config);
+ }
+
+ public List<RequiredOption<?>> getRequiredOptions() {
+ return this.requiredOptions;
+ }
+
+ protected Config parseConfig(String text) {
+ Config cfg = new Config();
+ try {
+ cfg.fromText(text);
+ } catch (ConfigInvalidException e) {
+ throw new IllegalStateException("Invalid configuration: " + text, e);
+ }
+ return cfg;
+ }
+
+ @SuppressWarnings("unchecked")
+ private void setRequiredOptions() {
+ for (RequiredOption<?> opt : requiredOptions) {
+ if (opt.getExpected() instanceof String) {
+ config.setString(
+ opt.getSection(), opt.getSubSection(), opt.getKey(), (String) opt.getExpected());
+ } else if (opt.getExpected() instanceof Boolean) {
+ config.setBoolean(
+ opt.getSection(), opt.getSubSection(), opt.getKey(), (Boolean) opt.getExpected());
+ } else if (opt.getExpected() instanceof Set) {
+ List<String> values =
+ new ArrayList<String>(
+ Arrays.asList(
+ config.getStringList(opt.getSection(), opt.getSubSection(), opt.getKey())));
+ List<String> expectedSet = new ArrayList<String>();
+ expectedSet.addAll((Set<String>) opt.getExpected());
+ expectedSet.removeAll(values);
+ values.addAll(expectedSet);
+ config.setStringList(opt.getSection(), opt.getSubSection(), opt.getKey(), values);
+ }
+ }
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/ConfigValidator.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/ConfigValidator.java
new file mode 100644
index 0000000..bc952a1
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/ConfigValidator.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gerrit.config;
+
+import java.util.List;
+import java.util.Set;
+import org.eclipse.jgit.lib.Config;
+
+public class ConfigValidator {
+ private final List<RequiredOption<?>> requiredOptions;
+
+ public ConfigValidator(List<RequiredOption<?>> requiredOptions) {
+ this.requiredOptions = requiredOptions;
+ }
+
+ public void check(Config cfg) throws InvalidGerritConfigException {
+ for (RequiredOption<?> opt : requiredOptions) {
+ checkOption(cfg, opt);
+ }
+ }
+
+ private void checkOption(Config cfg, RequiredOption<?> opt) throws InvalidGerritConfigException {
+ if (!optionExists(cfg, opt)) {
+ return;
+ }
+ if (opt.getExpected() instanceof Set) {
+ return;
+ } else {
+ String value = cfg.getString(opt.getSection(), opt.getSubSection(), opt.getKey());
+ if (isExpectedValue(value, opt)) {
+ return;
+ }
+ throw new InvalidGerritConfigException(value, opt);
+ }
+ }
+
+ private boolean optionExists(Config cfg, RequiredOption<?> opt) {
+ return cfg.getNames(opt.getSection(), opt.getSubSection()).contains(opt.getKey());
+ }
+
+ private boolean isExpectedValue(String value, RequiredOption<?> opt) {
+ return value.equals(opt.getExpected().toString());
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/InvalidGerritConfigException.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/InvalidGerritConfigException.java
new file mode 100644
index 0000000..6ab14bd
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/InvalidGerritConfigException.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gerrit.config;
+
+public class InvalidGerritConfigException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public InvalidGerritConfigException(String value, RequiredOption<?> opt) {
+ super(
+ String.format(
+ "Option %s.%s.%s set to unsupported value %s. Expected %s.",
+ opt.getSection(), opt.getSubSection(), opt.getKey(), value, opt.getExpected()));
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/RequiredOption.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/RequiredOption.java
new file mode 100644
index 0000000..9f4d22c
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/config/RequiredOption.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gerrit.config;
+
+public class RequiredOption<T> {
+ private final String section;
+ private final String subSection;
+ private final String key;
+ private final T expected;
+
+ public RequiredOption(String section, String subSection, String key, T expected) {
+ this.section = section;
+ this.subSection = subSection;
+ this.key = key;
+ this.expected = expected;
+ }
+
+ public RequiredOption(String section, String key, T expected) {
+ this(section, null, key, expected);
+ }
+
+ public String getSection() {
+ return section;
+ }
+
+ public String getSubSection() {
+ return subSection;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public T getExpected() {
+ return expected;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritConfigMap.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritConfigMap.java
new file mode 100644
index 0000000..22001b9
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritConfigMap.java
@@ -0,0 +1,91 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gerrit.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import com.google.gerrit.k8s.operator.v1alpha.gerrit.config.GerritConfigBuilder;
+import com.google.gerrit.k8s.operator.v1alpha.gerrit.config.HighAvailabilityPluginConfigBuilder;
+import com.google.gerrit.k8s.operator.v1alpha.gerrit.config.SpannerRefDbPluginConfigBuilder;
+import com.google.gerrit.k8s.operator.v1alpha.gerrit.config.ZookeeperRefDbPluginConfigBuilder;
+import io.fabric8.kubernetes.api.model.ConfigMap;
+import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+import java.util.Map;
+
+@KubernetesDependent(resourceDiscriminator = GerritConfigMapDiscriminator.class)
+public class GerritConfigMap extends CRUDKubernetesDependentResource<ConfigMap, Gerrit> {
+ private static final String DEFAULT_HEALTHCHECK_CONFIG =
+ "[healthcheck \"auth\"]\nenabled = false\n[healthcheck \"querychanges\"]\nenabled = false";
+
+ public GerritConfigMap() {
+ super(ConfigMap.class);
+ }
+
+ @Override
+ protected ConfigMap desired(Gerrit gerrit, Context<Gerrit> context) {
+ Map<String, String> gerritLabels =
+ GerritCluster.getLabels(
+ gerrit.getMetadata().getName(), getName(gerrit), this.getClass().getSimpleName());
+
+ Map<String, String> configFiles = gerrit.getSpec().getConfigFiles();
+
+ if (!configFiles.containsKey("gerrit.config")) {
+ configFiles.put("gerrit.config", "");
+ }
+
+ configFiles.put("gerrit.config", new GerritConfigBuilder(gerrit).build().toText());
+
+ if (gerrit.getSpec().isHighlyAvailablePrimary()) {
+ configFiles.put(
+ "high-availability.config",
+ new HighAvailabilityPluginConfigBuilder(gerrit).build().toText());
+ }
+
+ switch (gerrit.getSpec().getRefdb().getDatabase()) {
+ case ZOOKEEPER:
+ configFiles.put(
+ "zookeeper-refdb.config",
+ new ZookeeperRefDbPluginConfigBuilder(gerrit).build().toText());
+ break;
+ case SPANNER:
+ configFiles.put(
+ "spanner-refdb.config", new SpannerRefDbPluginConfigBuilder(gerrit).build().toText());
+ break;
+ default:
+ break;
+ }
+
+ if (!configFiles.containsKey("healthcheck.config")) {
+ configFiles.put("healthcheck.config", DEFAULT_HEALTHCHECK_CONFIG);
+ }
+
+ return new ConfigMapBuilder()
+ .withApiVersion("v1")
+ .withNewMetadata()
+ .withName(getName(gerrit))
+ .withNamespace(gerrit.getMetadata().getNamespace())
+ .withLabels(gerritLabels)
+ .endMetadata()
+ .withData(configFiles)
+ .build();
+ }
+
+ public static String getName(Gerrit gerrit) {
+ return String.format("%s-configmap", gerrit.getMetadata().getName());
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritConfigMapDiscriminator.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritConfigMapDiscriminator.java
new file mode 100644
index 0000000..6ec6b77
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritConfigMapDiscriminator.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gerrit.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import io.fabric8.kubernetes.api.model.ConfigMap;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.Optional;
+
+public class GerritConfigMapDiscriminator implements ResourceDiscriminator<ConfigMap, Gerrit> {
+ @Override
+ public Optional<ConfigMap> distinguish(
+ Class<ConfigMap> resource, Gerrit primary, Context<Gerrit> context) {
+ InformerEventSource<ConfigMap, Gerrit> ies =
+ (InformerEventSource<ConfigMap, Gerrit>)
+ context.eventSourceRetriever().getResourceEventSourceFor(ConfigMap.class);
+
+ return ies.get(
+ new ResourceID(GerritConfigMap.getName(primary), primary.getMetadata().getNamespace()));
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritInitConfigMap.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritInitConfigMap.java
new file mode 100644
index 0000000..3b9b8b4
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritInitConfigMap.java
@@ -0,0 +1,94 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gerrit.dependent;
+
+import static com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster.PLUGIN_CACHE_MOUNT_PATH;
+import static com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GlobalRefDbConfig.RefDatabase.SPANNER;
+import static com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GlobalRefDbConfig.RefDatabase.ZOOKEEPER;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritInitConfig;
+import io.fabric8.kubernetes.api.model.ConfigMap;
+import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+import java.util.Locale;
+import java.util.Map;
+
+@KubernetesDependent(resourceDiscriminator = GerritInitConfigMapDiscriminator.class)
+public class GerritInitConfigMap extends CRUDKubernetesDependentResource<ConfigMap, Gerrit> {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ public GerritInitConfigMap() {
+ super(ConfigMap.class);
+ }
+
+ @Override
+ protected ConfigMap desired(Gerrit gerrit, Context<Gerrit> context) {
+ Map<String, String> gerritLabels =
+ GerritCluster.getLabels(
+ gerrit.getMetadata().getName(), getName(gerrit), this.getClass().getSimpleName());
+
+ return new ConfigMapBuilder()
+ .withApiVersion("v1")
+ .withNewMetadata()
+ .withName(getName(gerrit))
+ .withNamespace(gerrit.getMetadata().getNamespace())
+ .withLabels(gerritLabels)
+ .endMetadata()
+ .withData(Map.of("gerrit-init.yaml", getGerritInitConfig(gerrit)))
+ .build();
+ }
+
+ private String getGerritInitConfig(Gerrit gerrit) {
+ GerritInitConfig config = new GerritInitConfig();
+ config.setPlugins(gerrit.getSpec().getPlugins());
+ config.setLibs(gerrit.getSpec().getLibs());
+ config.setPluginCacheEnabled(gerrit.getSpec().getStorage().getPluginCache().isEnabled());
+ config.setPluginCacheDir(PLUGIN_CACHE_MOUNT_PATH);
+ config.setHighlyAvailable(gerrit.getSpec().isHighlyAvailablePrimary());
+
+ switch (gerrit.getSpec().getRefdb().getDatabase()) {
+ case ZOOKEEPER:
+ config.setRefdb(ZOOKEEPER.toString().toLowerCase(Locale.US));
+ break;
+ case SPANNER:
+ config.setRefdb(SPANNER.toString().toLowerCase(Locale.US));
+ break;
+ default:
+ break;
+ }
+
+ ObjectMapper mapper =
+ new ObjectMapper(new YAMLFactory().disable(Feature.WRITE_DOC_START_MARKER));
+ try {
+ return mapper.writeValueAsString(config);
+ } catch (JsonProcessingException e) {
+ logger.atSevere().withCause(e).log("Could not serialize gerrit-init.config");
+ throw new IllegalStateException(e);
+ }
+ }
+
+ public static String getName(Gerrit gerrit) {
+ return String.format("%s-init-configmap", gerrit.getMetadata().getName());
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritInitConfigMapDiscriminator.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritInitConfigMapDiscriminator.java
new file mode 100644
index 0000000..5494f5a
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritInitConfigMapDiscriminator.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gerrit.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import io.fabric8.kubernetes.api.model.ConfigMap;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.Optional;
+
+public class GerritInitConfigMapDiscriminator implements ResourceDiscriminator<ConfigMap, Gerrit> {
+ @Override
+ public Optional<ConfigMap> distinguish(
+ Class<ConfigMap> resource, Gerrit primary, Context<Gerrit> context) {
+ InformerEventSource<ConfigMap, Gerrit> ies =
+ (InformerEventSource<ConfigMap, Gerrit>)
+ context.eventSourceRetriever().getResourceEventSourceFor(ConfigMap.class);
+
+ return ies.get(
+ new ResourceID(GerritInitConfigMap.getName(primary), primary.getMetadata().getNamespace()));
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritSecret.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritSecret.java
new file mode 100644
index 0000000..ac65dcd
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritSecret.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gerrit.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import io.fabric8.kubernetes.api.model.Secret;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@KubernetesDependent
+public class GerritSecret extends KubernetesDependentResource<Secret, Gerrit>
+ implements SecondaryToPrimaryMapper<Secret> {
+
+ public static final String CONTEXT_SECRET_VERSION_KEY = "gerrit-secret-version";
+
+ public GerritSecret() {
+ super(Secret.class);
+ }
+
+ @Override
+ public Set<ResourceID> toPrimaryResourceIDs(Secret secret) {
+ return client
+ .resources(Gerrit.class)
+ .inNamespace(secret.getMetadata().getNamespace())
+ .list()
+ .getItems()
+ .stream()
+ .filter(g -> g.getSpec().getSecretRef().equals(secret.getMetadata().getName()))
+ .map(g -> ResourceID.fromResource(g))
+ .collect(Collectors.toSet());
+ }
+
+ @Override
+ protected ReconcileResult<Secret> reconcile(
+ Gerrit primary, Secret actualResource, Context<Gerrit> context) {
+ Secret sec =
+ client
+ .secrets()
+ .inNamespace(primary.getMetadata().getNamespace())
+ .withName(primary.getSpec().getSecretRef())
+ .get();
+ if (sec != null) {
+ context
+ .managedDependentResourceContext()
+ .put(CONTEXT_SECRET_VERSION_KEY, sec.getMetadata().getResourceVersion());
+ }
+ return ReconcileResult.noOperation(actualResource);
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritService.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritService.java
new file mode 100644
index 0000000..888903c
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritService.java
@@ -0,0 +1,108 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gerrit.dependent;
+
+import static com.google.gerrit.k8s.operator.gerrit.dependent.GerritStatefulSet.HTTP_PORT;
+import static com.google.gerrit.k8s.operator.gerrit.dependent.GerritStatefulSet.SSH_PORT;
+
+import com.google.gerrit.k8s.operator.gerrit.GerritReconciler;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplate;
+import io.fabric8.kubernetes.api.model.Service;
+import io.fabric8.kubernetes.api.model.ServiceBuilder;
+import io.fabric8.kubernetes.api.model.ServicePort;
+import io.fabric8.kubernetes.api.model.ServicePortBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+@KubernetesDependent
+public class GerritService extends CRUDKubernetesDependentResource<Service, Gerrit> {
+ public static final String HTTP_PORT_NAME = "http";
+
+ public GerritService() {
+ super(Service.class);
+ }
+
+ @Override
+ protected Service desired(Gerrit gerrit, Context<Gerrit> context) {
+ return new ServiceBuilder()
+ .withApiVersion("v1")
+ .withNewMetadata()
+ .withName(getName(gerrit))
+ .withNamespace(gerrit.getMetadata().getNamespace())
+ .withLabels(getLabels(gerrit))
+ .endMetadata()
+ .withNewSpec()
+ .withType(gerrit.getSpec().getService().getType())
+ .withPorts(getServicePorts(gerrit))
+ .withSelector(GerritStatefulSet.getSelectorLabels(gerrit))
+ .endSpec()
+ .build();
+ }
+
+ public static String getName(Gerrit gerrit) {
+ return gerrit.getMetadata().getName();
+ }
+
+ public static String getName(String gerritName) {
+ return gerritName;
+ }
+
+ public static String getName(GerritTemplate gerrit) {
+ return gerrit.getMetadata().getName();
+ }
+
+ public static String getHostname(Gerrit gerrit) {
+ return getHostname(gerrit.getMetadata().getName(), gerrit.getMetadata().getNamespace());
+ }
+
+ public static String getHostname(String name, String namespace) {
+ return String.format("%s.%s.svc.cluster.local", name, namespace);
+ }
+
+ public static String getUrl(Gerrit gerrit) {
+ return String.format(
+ "http://%s:%s", getHostname(gerrit), gerrit.getSpec().getService().getHttpPort());
+ }
+
+ public static Map<String, String> getLabels(Gerrit gerrit) {
+ return GerritCluster.getLabels(
+ gerrit.getMetadata().getName(), "gerrit-service", GerritReconciler.class.getSimpleName());
+ }
+
+ private static List<ServicePort> getServicePorts(Gerrit gerrit) {
+ List<ServicePort> ports = new ArrayList<>();
+ ports.add(
+ new ServicePortBuilder()
+ .withName(HTTP_PORT_NAME)
+ .withPort(gerrit.getSpec().getService().getHttpPort())
+ .withNewTargetPort(HTTP_PORT)
+ .build());
+ if (gerrit.isSshEnabled()) {
+ ports.add(
+ new ServicePortBuilder()
+ .withName("ssh")
+ .withPort(gerrit.getSpec().getService().getSshPort())
+ .withNewTargetPort(SSH_PORT)
+ .build());
+ }
+ return ports;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritStatefulSet.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritStatefulSet.java
new file mode 100644
index 0000000..d319f4c
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/dependent/GerritStatefulSet.java
@@ -0,0 +1,364 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gerrit.dependent;
+
+import static com.google.gerrit.k8s.operator.gerrit.dependent.GerritSecret.CONTEXT_SECRET_VERSION_KEY;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.k8s.operator.gerrit.GerritReconciler;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.ContainerImageConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.NfsWorkaroundConfig;
+import io.fabric8.kubernetes.api.model.Container;
+import io.fabric8.kubernetes.api.model.ContainerPort;
+import io.fabric8.kubernetes.api.model.EnvVar;
+import io.fabric8.kubernetes.api.model.EnvVarBuilder;
+import io.fabric8.kubernetes.api.model.Volume;
+import io.fabric8.kubernetes.api.model.VolumeBuilder;
+import io.fabric8.kubernetes.api.model.VolumeMount;
+import io.fabric8.kubernetes.api.model.VolumeMountBuilder;
+import io.fabric8.kubernetes.api.model.apps.StatefulSet;
+import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+import java.sql.Timestamp;
+import java.text.SimpleDateFormat;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+@KubernetesDependent
+public class GerritStatefulSet extends CRUDKubernetesDependentResource<StatefulSet, Gerrit> {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private static final SimpleDateFormat RFC3339 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
+
+ private static final String SITE_VOLUME_NAME = "gerrit-site";
+ public static final int HTTP_PORT = 8080;
+ public static final int SSH_PORT = 29418;
+ public static final int JGROUPS_PORT = 7800;
+ public static final int DEBUG_PORT = 8000;
+
+ public GerritStatefulSet() {
+ super(StatefulSet.class);
+ }
+
+ @Override
+ protected StatefulSet desired(Gerrit gerrit, Context<Gerrit> context) {
+ StatefulSetBuilder stsBuilder = new StatefulSetBuilder();
+
+ List<Container> initContainers = new ArrayList<>();
+
+ NfsWorkaroundConfig nfsWorkaround =
+ gerrit.getSpec().getStorage().getStorageClasses().getNfsWorkaround();
+ if (nfsWorkaround.isEnabled() && nfsWorkaround.isChownOnStartup()) {
+ boolean hasIdmapdConfig =
+ gerrit.getSpec().getStorage().getStorageClasses().getNfsWorkaround().getIdmapdConfig()
+ != null;
+ ContainerImageConfig images = gerrit.getSpec().getContainerImages();
+
+ if (gerrit.getSpec().isHighlyAvailablePrimary()) {
+
+ initContainers.add(
+ GerritCluster.createNfsInitContainer(
+ hasIdmapdConfig, images, List.of(GerritCluster.getHAShareVolumeMount())));
+ } else {
+ initContainers.add(GerritCluster.createNfsInitContainer(hasIdmapdConfig, images));
+ }
+ }
+
+ Map<String, String> replicaSetAnnotations = new HashMap<>();
+ if (gerrit.getStatus() != null && isGerritRestartRequired(gerrit, context)) {
+ replicaSetAnnotations.put(
+ "kubectl.kubernetes.io/restartedAt", RFC3339.format(Timestamp.from(Instant.now())));
+ } else {
+ Optional<StatefulSet> existingSts = context.getSecondaryResource(StatefulSet.class);
+ if (existingSts.isPresent()) {
+ Map<String, String> existingAnnotations =
+ existingSts.get().getSpec().getTemplate().getMetadata().getAnnotations();
+ if (existingAnnotations.containsKey("kubectl.kubernetes.io/restartedAt")) {
+ replicaSetAnnotations.put(
+ "kubectl.kubernetes.io/restartedAt",
+ existingAnnotations.get("kubectl.kubernetes.io/restartedAt"));
+ }
+ }
+ }
+
+ stsBuilder
+ .withApiVersion("apps/v1")
+ .withNewMetadata()
+ .withName(gerrit.getMetadata().getName())
+ .withNamespace(gerrit.getMetadata().getNamespace())
+ .withLabels(getLabels(gerrit))
+ .endMetadata()
+ .withNewSpec()
+ .withServiceName(GerritService.getName(gerrit))
+ .withReplicas(gerrit.getSpec().getReplicas())
+ .withNewUpdateStrategy()
+ .withNewRollingUpdate()
+ .withPartition(gerrit.getSpec().getUpdatePartition())
+ .endRollingUpdate()
+ .endUpdateStrategy()
+ .withNewSelector()
+ .withMatchLabels(getSelectorLabels(gerrit))
+ .endSelector()
+ .withNewTemplate()
+ .withNewMetadata()
+ .withAnnotations(replicaSetAnnotations)
+ .withLabels(getLabels(gerrit))
+ .endMetadata()
+ .withNewSpec()
+ .withServiceAccount(gerrit.getSpec().getServiceAccount())
+ .withTolerations(gerrit.getSpec().getTolerations())
+ .withTopologySpreadConstraints(gerrit.getSpec().getTopologySpreadConstraints())
+ .withAffinity(gerrit.getSpec().getAffinity())
+ .withPriorityClassName(gerrit.getSpec().getPriorityClassName())
+ .withTerminationGracePeriodSeconds(gerrit.getSpec().getGracefulStopTimeout())
+ .addAllToImagePullSecrets(gerrit.getSpec().getContainerImages().getImagePullSecrets())
+ .withNewSecurityContext()
+ .withFsGroup(100L)
+ .endSecurityContext()
+ .addNewInitContainer()
+ .withName("gerrit-init")
+ .withEnv(getEnvVars(gerrit))
+ .withImagePullPolicy(gerrit.getSpec().getContainerImages().getImagePullPolicy())
+ .withImage(
+ gerrit.getSpec().getContainerImages().getGerritImages().getFullImageName("gerrit-init"))
+ .withResources(gerrit.getSpec().getResources())
+ .addAllToVolumeMounts(getVolumeMounts(gerrit, true))
+ .endInitContainer()
+ .addAllToInitContainers(initContainers)
+ .addNewContainer()
+ .withName("gerrit")
+ .withImagePullPolicy(gerrit.getSpec().getContainerImages().getImagePullPolicy())
+ .withImage(
+ gerrit.getSpec().getContainerImages().getGerritImages().getFullImageName("gerrit"))
+ .withNewLifecycle()
+ .withNewPreStop()
+ .withNewExec()
+ .withCommand(
+ "/bin/ash/", "-c", "kill -2 $(pidof java) && tail --pid=$(pidof java) -f /dev/null")
+ .endExec()
+ .endPreStop()
+ .endLifecycle()
+ .withEnv(getEnvVars(gerrit))
+ .withPorts(getContainerPorts(gerrit))
+ .withResources(gerrit.getSpec().getResources())
+ .withStartupProbe(gerrit.getSpec().getStartupProbe())
+ .withReadinessProbe(gerrit.getSpec().getReadinessProbe())
+ .withLivenessProbe(gerrit.getSpec().getLivenessProbe())
+ .addAllToVolumeMounts(getVolumeMounts(gerrit, false))
+ .endContainer()
+ .addAllToVolumes(getVolumes(gerrit))
+ .endSpec()
+ .endTemplate()
+ .addNewVolumeClaimTemplate()
+ .withNewMetadata()
+ .withName(SITE_VOLUME_NAME)
+ .withLabels(getSelectorLabels(gerrit))
+ .endMetadata()
+ .withNewSpec()
+ .withAccessModes("ReadWriteOnce")
+ .withNewResources()
+ .withRequests(Map.of("storage", gerrit.getSpec().getSite().getSize()))
+ .endResources()
+ .withStorageClassName(gerrit.getSpec().getStorage().getStorageClasses().getReadWriteOnce())
+ .endSpec()
+ .endVolumeClaimTemplate()
+ .endSpec();
+
+ return stsBuilder.build();
+ }
+
+ private static String getComponentName(Gerrit gerrit) {
+ return String.format("gerrit-statefulset-%s", gerrit.getMetadata().getName());
+ }
+
+ public static Map<String, String> getSelectorLabels(Gerrit gerrit) {
+ return GerritCluster.getSelectorLabels(
+ gerrit.getMetadata().getName(), getComponentName(gerrit));
+ }
+
+ private static Map<String, String> getLabels(Gerrit gerrit) {
+ return GerritCluster.getLabels(
+ gerrit.getMetadata().getName(),
+ getComponentName(gerrit),
+ GerritReconciler.class.getSimpleName());
+ }
+
+ private Set<Volume> getVolumes(Gerrit gerrit) {
+ Set<Volume> volumes = new HashSet<>();
+
+ volumes.add(
+ GerritCluster.getSharedVolume(
+ gerrit.getSpec().getStorage().getSharedStorage().getExternalPVC()));
+
+ volumes.add(
+ new VolumeBuilder()
+ .withName("gerrit-init-config")
+ .withNewConfigMap()
+ .withName(GerritInitConfigMap.getName(gerrit))
+ .endConfigMap()
+ .build());
+
+ volumes.add(
+ new VolumeBuilder()
+ .withName("gerrit-config")
+ .withNewConfigMap()
+ .withName(GerritConfigMap.getName(gerrit))
+ .endConfigMap()
+ .build());
+
+ volumes.add(
+ new VolumeBuilder()
+ .withName(gerrit.getSpec().getSecretRef())
+ .withNewSecret()
+ .withSecretName(gerrit.getSpec().getSecretRef())
+ .endSecret()
+ .build());
+
+ NfsWorkaroundConfig nfsWorkaround =
+ gerrit.getSpec().getStorage().getStorageClasses().getNfsWorkaround();
+ if (nfsWorkaround.isEnabled() && nfsWorkaround.getIdmapdConfig() != null) {
+ volumes.add(GerritCluster.getNfsImapdConfigVolume());
+ }
+
+ return volumes;
+ }
+
+ private Set<VolumeMount> getVolumeMounts(Gerrit gerrit, boolean isInitContainer) {
+ Set<VolumeMount> volumeMounts = new HashSet<>();
+ volumeMounts.add(
+ new VolumeMountBuilder().withName(SITE_VOLUME_NAME).withMountPath("/var/gerrit").build());
+ if (gerrit.getSpec().isHighlyAvailablePrimary()) {
+ volumeMounts.add(GerritCluster.getHAShareVolumeMount());
+ }
+ volumeMounts.add(GerritCluster.getGitRepositoriesVolumeMount());
+ volumeMounts.add(GerritCluster.getLogsVolumeMount());
+ volumeMounts.add(
+ new VolumeMountBuilder()
+ .withName("gerrit-config")
+ .withMountPath("/var/mnt/etc/config")
+ .build());
+
+ volumeMounts.add(
+ new VolumeMountBuilder()
+ .withName(gerrit.getSpec().getSecretRef())
+ .withMountPath("/var/mnt/etc/secret")
+ .build());
+
+ if (isInitContainer) {
+ volumeMounts.add(
+ new VolumeMountBuilder()
+ .withName("gerrit-init-config")
+ .withMountPath("/var/config")
+ .build());
+
+ if (gerrit.getSpec().getStorage().getPluginCache().isEnabled()
+ && gerrit.getSpec().getPlugins().stream().anyMatch(p -> !p.isPackagedPlugin())) {
+ volumeMounts.add(GerritCluster.getPluginCacheVolumeMount());
+ }
+ }
+
+ NfsWorkaroundConfig nfsWorkaround =
+ gerrit.getSpec().getStorage().getStorageClasses().getNfsWorkaround();
+ if (nfsWorkaround.isEnabled() && nfsWorkaround.getIdmapdConfig() != null) {
+ volumeMounts.add(GerritCluster.getNfsImapdConfigVolumeMount());
+ }
+
+ return volumeMounts;
+ }
+
+ private List<ContainerPort> getContainerPorts(Gerrit gerrit) {
+ List<ContainerPort> containerPorts = new ArrayList<>();
+ containerPorts.add(new ContainerPort(HTTP_PORT, null, null, "http", null));
+
+ if (gerrit.isSshEnabled()) {
+ containerPorts.add(new ContainerPort(SSH_PORT, null, null, "ssh", null));
+ }
+
+ if (gerrit.getSpec().isHighlyAvailablePrimary()) {
+ containerPorts.add(new ContainerPort(JGROUPS_PORT, null, null, "jgroups", null));
+ }
+
+ if (gerrit.getSpec().getDebug().isEnabled()) {
+ containerPorts.add(new ContainerPort(DEBUG_PORT, null, null, "debug", null));
+ }
+
+ return containerPorts;
+ }
+
+ private List<EnvVar> getEnvVars(Gerrit gerrit) {
+ List<EnvVar> envVars = new ArrayList<>();
+ envVars.add(GerritCluster.getPodNameEnvVar());
+ if (gerrit.getSpec().isHighlyAvailablePrimary()) {
+ envVars.add(
+ new EnvVarBuilder()
+ .withName("GERRIT_URL")
+ .withValue(
+ String.format(
+ "http://$(POD_NAME).%s:%s", GerritService.getHostname(gerrit), HTTP_PORT))
+ .build());
+ }
+ return envVars;
+ }
+
+ private boolean isGerritRestartRequired(Gerrit gerrit, Context<Gerrit> context) {
+ if (wasConfigMapUpdated(GerritInitConfigMap.getName(gerrit), gerrit)
+ || wasConfigMapUpdated(GerritConfigMap.getName(gerrit), gerrit)) {
+ return true;
+ }
+
+ String secretName = gerrit.getSpec().getSecretRef();
+ Optional<String> gerritSecret =
+ context.managedDependentResourceContext().get(CONTEXT_SECRET_VERSION_KEY, String.class);
+ if (gerritSecret.isPresent()) {
+ String secVersion = gerritSecret.get();
+ if (!secVersion.equals(gerrit.getStatus().getAppliedSecretVersions().get(secretName))) {
+ logger.atFine().log(
+ "Looking up Secret: %s; Installed secret resource version: %s; Resource version known to Gerrit: %s",
+ secretName, secVersion, gerrit.getStatus().getAppliedSecretVersions().get(secretName));
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean wasConfigMapUpdated(String configMapName, Gerrit gerrit) {
+ String configMapVersion =
+ client
+ .configMaps()
+ .inNamespace(gerrit.getMetadata().getNamespace())
+ .withName(configMapName)
+ .get()
+ .getMetadata()
+ .getResourceVersion();
+ String knownConfigMapVersion =
+ gerrit.getStatus().getAppliedConfigMapVersions().get(configMapName);
+ if (!configMapVersion.equals(knownConfigMapVersion)) {
+ logger.atInfo().log(
+ "Looking up ConfigMap: %s; Installed configmap resource version: %s; Resource version known to Gerrit: %s",
+ configMapName, configMapVersion, knownConfigMapVersion);
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollectionConflictException.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollectionConflictException.java
new file mode 100644
index 0000000..8d6bb26
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollectionConflictException.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gitgc;
+
+import java.util.Collection;
+
+public class GitGarbageCollectionConflictException extends RuntimeException {
+
+ private static final long serialVersionUID = 1L;
+
+ public GitGarbageCollectionConflictException(Collection<String> projectsIntercept) {
+ super(String.format("Found conflicting GC jobs for projects: %s", projectsIntercept));
+ }
+
+ public GitGarbageCollectionConflictException(String s) {
+ super(s);
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollectionReconciler.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollectionReconciler.java
new file mode 100644
index 0000000..8d28fea
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollectionReconciler.java
@@ -0,0 +1,152 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gitgc;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.k8s.operator.gitgc.dependent.GitGarbageCollectionCronJob;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gitgc.GitGarbageCollection;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gitgc.GitGarbageCollectionStatus;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gitgc.GitGarbageCollectionStatus.GitGcState;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler;
+import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusUpdateControl;
+import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
+import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer;
+import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
+import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.EventSource;
+import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Singleton
+@ControllerConfiguration
+public class GitGarbageCollectionReconciler
+ implements Reconciler<GitGarbageCollection>,
+ EventSourceInitializer<GitGarbageCollection>,
+ ErrorStatusHandler<GitGarbageCollection> {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private final KubernetesClient client;
+
+ private GitGarbageCollectionCronJob dependentCronJob;
+
+ @Inject
+ public GitGarbageCollectionReconciler(KubernetesClient client) {
+ this.client = client;
+ this.dependentCronJob = new GitGarbageCollectionCronJob();
+ this.dependentCronJob.setKubernetesClient(client);
+ }
+
+ @Override
+ public Map<String, EventSource> prepareEventSources(
+ EventSourceContext<GitGarbageCollection> context) {
+ final SecondaryToPrimaryMapper<GitGarbageCollection> specificProjectGitGcMapper =
+ (GitGarbageCollection gc) ->
+ context
+ .getPrimaryCache()
+ .list(gitGc -> gitGc.getSpec().getProjects().isEmpty())
+ .map(ResourceID::fromResource)
+ .collect(Collectors.toSet());
+
+ InformerEventSource<GitGarbageCollection, GitGarbageCollection> gitGcEventSource =
+ new InformerEventSource<>(
+ InformerConfiguration.from(GitGarbageCollection.class, context)
+ .withSecondaryToPrimaryMapper(specificProjectGitGcMapper)
+ .build(),
+ context);
+
+ final SecondaryToPrimaryMapper<GerritCluster> gerritClusterMapper =
+ (GerritCluster cluster) ->
+ context
+ .getPrimaryCache()
+ .list(gitGc -> gitGc.getSpec().getCluster().equals(cluster.getMetadata().getName()))
+ .map(ResourceID::fromResource)
+ .collect(Collectors.toSet());
+
+ InformerEventSource<GerritCluster, GitGarbageCollection> gerritClusterEventSource =
+ new InformerEventSource<>(
+ InformerConfiguration.from(GerritCluster.class, context)
+ .withSecondaryToPrimaryMapper(gerritClusterMapper)
+ .build(),
+ context);
+
+ return EventSourceInitializer.nameEventSources(
+ gitGcEventSource, gerritClusterEventSource, dependentCronJob.initEventSource(context));
+ }
+
+ @Override
+ public UpdateControl<GitGarbageCollection> reconcile(
+ GitGarbageCollection gitGc, Context<GitGarbageCollection> context) {
+ if (gitGc.getSpec().getProjects().isEmpty()) {
+ gitGc = excludeProjectsHandledSeparately(gitGc);
+ }
+
+ dependentCronJob.reconcile(gitGc, context);
+ return UpdateControl.updateStatus(updateGitGcStatus(gitGc));
+ }
+
+ private GitGarbageCollection updateGitGcStatus(GitGarbageCollection gitGc) {
+ GitGarbageCollectionStatus status = gitGc.getStatus();
+ if (status == null) {
+ status = new GitGarbageCollectionStatus();
+ }
+ status.setReplicateAll(gitGc.getSpec().getProjects().isEmpty());
+ status.setState(GitGcState.ACTIVE);
+ gitGc.setStatus(status);
+ return gitGc;
+ }
+
+ private GitGarbageCollection excludeProjectsHandledSeparately(GitGarbageCollection currentGitGc) {
+ List<GitGarbageCollection> gitGcs =
+ client
+ .resources(GitGarbageCollection.class)
+ .inNamespace(currentGitGc.getMetadata().getNamespace())
+ .list()
+ .getItems();
+ gitGcs.remove(currentGitGc);
+ GitGarbageCollectionStatus currentGitGcStatus = currentGitGc.getStatus();
+ currentGitGcStatus.resetExcludedProjects();
+ for (GitGarbageCollection gc : gitGcs) {
+ currentGitGcStatus.excludeProjects(gc.getSpec().getProjects());
+ }
+ currentGitGc.setStatus(currentGitGcStatus);
+
+ return currentGitGc;
+ }
+
+ @Override
+ public ErrorStatusUpdateControl<GitGarbageCollection> updateErrorStatus(
+ GitGarbageCollection gitGc, Context<GitGarbageCollection> context, Exception e) {
+ GitGarbageCollectionStatus status = new GitGarbageCollectionStatus();
+ if (e instanceof GitGarbageCollectionConflictException) {
+ status.setState(GitGcState.CONFLICT);
+ } else {
+ logger.atSevere().withCause(e).log("Failed reconcile with message: %s", e.getMessage());
+ status.setState(GitGcState.ERROR);
+ }
+ gitGc.setStatus(status);
+
+ return ErrorStatusUpdateControl.updateStatus(gitGc);
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/dependent/GitGarbageCollectionCronJob.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/dependent/GitGarbageCollectionCronJob.java
new file mode 100644
index 0000000..254bbab
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/dependent/GitGarbageCollectionCronJob.java
@@ -0,0 +1,186 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gitgc.dependent;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gitgc.GitGarbageCollection;
+import io.fabric8.kubernetes.api.model.Container;
+import io.fabric8.kubernetes.api.model.ContainerBuilder;
+import io.fabric8.kubernetes.api.model.Volume;
+import io.fabric8.kubernetes.api.model.VolumeMount;
+import io.fabric8.kubernetes.api.model.batch.v1.CronJob;
+import io.fabric8.kubernetes.api.model.batch.v1.CronJobBuilder;
+import io.fabric8.kubernetes.api.model.batch.v1.JobTemplateSpec;
+import io.fabric8.kubernetes.api.model.batch.v1.JobTemplateSpecBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+public class GitGarbageCollectionCronJob
+ extends CRUDKubernetesDependentResource<CronJob, GitGarbageCollection> {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ public GitGarbageCollectionCronJob() {
+ super(CronJob.class);
+ }
+
+ @Override
+ protected CronJob desired(GitGarbageCollection gitGc, Context<GitGarbageCollection> context) {
+ String ns = gitGc.getMetadata().getNamespace();
+ String name = gitGc.getMetadata().getName();
+ GerritCluster gerritCluster =
+ client
+ .resources(GerritCluster.class)
+ .inNamespace(ns)
+ .withName(gitGc.getSpec().getCluster())
+ .get();
+ logger.atInfo().log("Reconciling GitGc with name: %s/%s", ns, name);
+
+ Map<String, String> gitGcLabels =
+ gerritCluster.getLabels("GitGc", this.getClass().getSimpleName());
+
+ List<Container> initContainers = new ArrayList<>();
+ if (gerritCluster.getSpec().getStorage().getStorageClasses().getNfsWorkaround().isEnabled()
+ && gerritCluster
+ .getSpec()
+ .getStorage()
+ .getStorageClasses()
+ .getNfsWorkaround()
+ .isChownOnStartup()) {
+ initContainers.add(gerritCluster.createNfsInitContainer());
+ }
+
+ JobTemplateSpec gitGcJobTemplate =
+ new JobTemplateSpecBuilder()
+ .withNewSpec()
+ .withNewTemplate()
+ .withNewMetadata()
+ .withAnnotations(
+ Map.of(
+ "sidecar.istio.io/inject",
+ "false",
+ "cluster-autoscaler.kubernetes.io/safe-to-evict",
+ "false"))
+ .withLabels(gitGcLabels)
+ .endMetadata()
+ .withNewSpec()
+ .withTolerations(gitGc.getSpec().getTolerations())
+ .withAffinity(gitGc.getSpec().getAffinity())
+ .addAllToImagePullSecrets(
+ gerritCluster.getSpec().getContainerImages().getImagePullSecrets())
+ .withRestartPolicy("OnFailure")
+ .withNewSecurityContext()
+ .withFsGroup(100L)
+ .endSecurityContext()
+ .addToContainers(buildGitGcContainer(gitGc, gerritCluster))
+ .withVolumes(getVolumes(gerritCluster))
+ .endSpec()
+ .endTemplate()
+ .endSpec()
+ .build();
+
+ return new CronJobBuilder()
+ .withApiVersion("batch/v1")
+ .withNewMetadata()
+ .withNamespace(ns)
+ .withName(name)
+ .withLabels(gitGcLabels)
+ .withAnnotations(
+ Collections.singletonMap("app.kubernetes.io/managed-by", "gerrit-operator"))
+ .addNewOwnerReference()
+ .withApiVersion(gitGc.getApiVersion())
+ .withKind(gitGc.getKind())
+ .withName(name)
+ .withUid(gitGc.getMetadata().getUid())
+ .endOwnerReference()
+ .endMetadata()
+ .withNewSpec()
+ .withSchedule(gitGc.getSpec().getSchedule())
+ .withConcurrencyPolicy("Forbid")
+ .withJobTemplate(gitGcJobTemplate)
+ .endSpec()
+ .build();
+ }
+
+ private Container buildGitGcContainer(GitGarbageCollection gitGc, GerritCluster gerritCluster) {
+ List<VolumeMount> volumeMounts =
+ List.of(
+ GerritCluster.getGitRepositoriesVolumeMount("/var/gerrit/git"),
+ GerritCluster.getLogsVolumeMount("/var/log/git"));
+
+ if (gerritCluster.getSpec().getStorage().getStorageClasses().getNfsWorkaround().isEnabled()
+ && gerritCluster
+ .getSpec()
+ .getStorage()
+ .getStorageClasses()
+ .getNfsWorkaround()
+ .getIdmapdConfig()
+ != null) {
+ volumeMounts.add(GerritCluster.getNfsImapdConfigVolumeMount());
+ }
+
+ ContainerBuilder gitGcContainerBuilder =
+ new ContainerBuilder()
+ .withName("git-gc")
+ .withImagePullPolicy(gerritCluster.getSpec().getContainerImages().getImagePullPolicy())
+ .withImage(
+ gerritCluster
+ .getSpec()
+ .getContainerImages()
+ .getGerritImages()
+ .getFullImageName("git-gc"))
+ .withResources(gitGc.getSpec().getResources())
+ .withEnv(GerritCluster.getPodNameEnvVar())
+ .withVolumeMounts(volumeMounts);
+
+ ArrayList<String> args = new ArrayList<>();
+ for (String project : gitGc.getSpec().getProjects()) {
+ args.add("-p");
+ args.add(project);
+ }
+ for (String project : gitGc.getStatus().getExcludedProjects()) {
+ args.add("-s");
+ args.add(project);
+ }
+ gitGcContainerBuilder.addAllToArgs(args);
+
+ return gitGcContainerBuilder.build();
+ }
+
+ private List<Volume> getVolumes(GerritCluster gerritCluster) {
+ List<Volume> volumes = new ArrayList<>();
+
+ volumes.add(
+ GerritCluster.getSharedVolume(
+ gerritCluster.getSpec().getStorage().getSharedStorage().getExternalPVC()));
+
+ if (gerritCluster.getSpec().getStorage().getStorageClasses().getNfsWorkaround().isEnabled()) {
+ if (gerritCluster
+ .getSpec()
+ .getStorage()
+ .getStorageClasses()
+ .getNfsWorkaround()
+ .getIdmapdConfig()
+ != null) {
+ volumes.add(GerritCluster.getNfsImapdConfigVolume());
+ }
+ }
+ return volumes;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/Constants.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/Constants.java
new file mode 100644
index 0000000..1a7f3b2
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/Constants.java
@@ -0,0 +1,22 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.package com.google.gerrit.k8s.operator.network;
+
+package com.google.gerrit.k8s.operator.network;
+
+public class Constants {
+ public static String UPLOAD_PACK_URL_PATTERN = "/.*/git-upload-pack";
+ public static String INFO_REFS_PATTERN = "/.*/info/refs";
+ public static String RECEIVE_PACK_URL_PATTERN = "/.*/git-receive-pack";
+ public static String PROJECTS_URL_PATTERN = "/a/projects/.*";
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/GerritClusterIngressCondition.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/GerritClusterIngressCondition.java
new file mode 100644
index 0000000..9077f1b
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/GerritClusterIngressCondition.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.fabric8.kubernetes.api.model.networking.v1.Ingress;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
+
+public class GerritClusterIngressCondition implements Condition<Ingress, GerritNetwork> {
+
+ @Override
+ public boolean isMet(
+ DependentResource<Ingress, GerritNetwork> dependentResource,
+ GerritNetwork gerritNetwork,
+ Context<GerritNetwork> context) {
+ return gerritNetwork.getSpec().getIngress().isEnabled()
+ && (gerritNetwork.hasReceiver() || gerritNetwork.hasGerrits());
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/GerritNetworkReconcilerProvider.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/GerritNetworkReconcilerProvider.java
new file mode 100644
index 0000000..a614e92
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/GerritNetworkReconcilerProvider.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network;
+
+import com.google.gerrit.k8s.operator.network.ambassador.GerritAmbassadorReconciler;
+import com.google.gerrit.k8s.operator.network.ingress.GerritIngressReconciler;
+import com.google.gerrit.k8s.operator.network.istio.GerritIstioReconciler;
+import com.google.gerrit.k8s.operator.network.none.GerritNoIngressReconciler;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.name.Named;
+import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
+
+public class GerritNetworkReconcilerProvider implements Provider<Reconciler<GerritNetwork>> {
+ private final IngressType ingressType;
+
+ @Inject
+ public GerritNetworkReconcilerProvider(@Named("IngressType") IngressType ingressType) {
+ this.ingressType = ingressType;
+ }
+
+ @Override
+ public Reconciler<GerritNetwork> get() {
+ switch (ingressType) {
+ case INGRESS:
+ return new GerritIngressReconciler();
+ case ISTIO:
+ return new GerritIstioReconciler();
+ case AMBASSADOR:
+ return new GerritAmbassadorReconciler();
+ default:
+ return new GerritNoIngressReconciler();
+ }
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/IngressType.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/IngressType.java
new file mode 100644
index 0000000..9c5383c
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/IngressType.java
@@ -0,0 +1,22 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network;
+
+public enum IngressType {
+ NONE,
+ INGRESS,
+ ISTIO,
+ AMBASSADOR
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/GerritAmbassadorReconciler.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/GerritAmbassadorReconciler.java
new file mode 100644
index 0000000..34dc9db
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/GerritAmbassadorReconciler.java
@@ -0,0 +1,169 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador;
+
+import static com.google.gerrit.k8s.operator.network.ambassador.GerritAmbassadorReconciler.MAPPING_EVENT_SOURCE;
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterHost.GERRIT_HOST;
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMapping.GERRIT_MAPPING;
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingGETReplica.GERRIT_MAPPING_GET_REPLICA;
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingPOSTReplica.GERRIT_MAPPING_POST_REPLICA;
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingPrimary.GERRIT_MAPPING_PRIMARY;
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingReceiver.GERRIT_MAPPING_RECEIVER;
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingReceiverGET.GERRIT_MAPPING_RECEIVER_GET;
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterTLSContext.GERRIT_TLS_CONTEXT;
+
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.CreateHostCondition;
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterHost;
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMapping;
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingGETReplica;
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingPOSTReplica;
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingPrimary;
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingReceiver;
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingReceiverGET;
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterTLSContext;
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.LoadBalanceCondition;
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.ReceiverMappingCondition;
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.SingleMappingCondition;
+import com.google.gerrit.k8s.operator.network.ambassador.dependent.TLSContextCondition;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import com.google.inject.Singleton;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
+import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer;
+import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
+import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
+import io.javaoperatorsdk.operator.processing.event.source.EventSource;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Provides an Ambassador-based implementation for GerritNetworkReconciler.
+ *
+ * <p>Creates and manages Ambassador Custom Resources using the "managed dependent resources"
+ * approach in josdk. Since multiple dependent resources of the same type (`Mapping`) need to be
+ * created, "resource discriminators" are used for each of the different Mapping dependent
+ * resources.
+ *
+ * <p>Ambassador custom resource POJOs are generated via the `java-generator-maven-plugin` in the
+ * fabric8 project.
+ *
+ * <p>Mapping logic
+ *
+ * <p>The Mappings are created based on the composition of Gerrit instances in the GerritCluster.
+ *
+ * <p>There are three cases:
+ *
+ * <p>1. 0 Primary 1 Replica
+ *
+ * <p>Direct all traffic (read/write) to the Replica
+ *
+ * <p>2. 1 Primary 0 Replica
+ *
+ * <p>Direct all traffic (read/write) to the Primary
+ *
+ * <p>3. 1 Primary 1 Replica
+ *
+ * <p>Direct write traffic to Primary and read traffic to Replica. To capture this requirement,
+ * three different Mappings have to be created.
+ *
+ * <p>Note: git fetch/clone operations result in two HTTP requests to the git server. The first is
+ * of the form `GET /my-test-repo/info/refs?service=git-upload-pack` and the second is of the form
+ * `POST /my-test-repo/git-upload-pack`.
+ *
+ * <p>Note: git push operations result in two HTTP requests to the git server. The first is of the
+ * form `GET /my-test-repo/info/refs?service=git-receive-pack` and the second is of the form `POST
+ * /my-test-repo/git-receive-pack`.
+ *
+ * <p>If a Receiver is part of the GerritCluster, additional mappings are created such that all
+ * requests that the replication plugin sends to the `adminUrl` [1] are routed to the Receiver. This
+ * includes `git push` related `GET` and `POST` requests, and requests to the `/projects` REST API
+ * endpoints.
+ *
+ * <p>[1]
+ * https://gerrit.googlesource.com/plugins/replication/+/refs/heads/master/src/main/resources/Documentation/config.md
+ */
+@Singleton
+@ControllerConfiguration(
+ dependents = {
+ @Dependent(
+ name = GERRIT_MAPPING,
+ type = GerritClusterMapping.class,
+ // Cluster has only either Primary or Replica instance
+ reconcilePrecondition = SingleMappingCondition.class,
+ useEventSourceWithName = MAPPING_EVENT_SOURCE),
+ @Dependent(
+ name = GERRIT_MAPPING_POST_REPLICA,
+ type = GerritClusterMappingPOSTReplica.class,
+ // Cluster has both Primary and Replica instances
+ reconcilePrecondition = LoadBalanceCondition.class,
+ useEventSourceWithName = MAPPING_EVENT_SOURCE),
+ @Dependent(
+ name = GERRIT_MAPPING_GET_REPLICA,
+ type = GerritClusterMappingGETReplica.class,
+ reconcilePrecondition = LoadBalanceCondition.class,
+ useEventSourceWithName = MAPPING_EVENT_SOURCE),
+ @Dependent(
+ name = GERRIT_MAPPING_PRIMARY,
+ type = GerritClusterMappingPrimary.class,
+ reconcilePrecondition = LoadBalanceCondition.class,
+ useEventSourceWithName = MAPPING_EVENT_SOURCE),
+ @Dependent(
+ name = GERRIT_MAPPING_RECEIVER,
+ type = GerritClusterMappingReceiver.class,
+ reconcilePrecondition = ReceiverMappingCondition.class,
+ useEventSourceWithName = MAPPING_EVENT_SOURCE),
+ @Dependent(
+ name = GERRIT_MAPPING_RECEIVER_GET,
+ type = GerritClusterMappingReceiverGET.class,
+ reconcilePrecondition = ReceiverMappingCondition.class,
+ useEventSourceWithName = MAPPING_EVENT_SOURCE),
+ @Dependent(
+ name = GERRIT_TLS_CONTEXT,
+ type = GerritClusterTLSContext.class,
+ reconcilePrecondition = TLSContextCondition.class),
+ @Dependent(
+ name = GERRIT_HOST,
+ type = GerritClusterHost.class,
+ reconcilePrecondition = CreateHostCondition.class),
+ })
+public class GerritAmbassadorReconciler
+ implements Reconciler<GerritNetwork>, EventSourceInitializer<GerritNetwork> {
+
+ public static final String MAPPING_EVENT_SOURCE = "mapping-event-source";
+
+ // Because we have multiple dependent resources of the same type `Mapping`, we need to specify
+ // a named event source.
+ @Override
+ public Map<String, EventSource> prepareEventSources(EventSourceContext<GerritNetwork> context) {
+ InformerEventSource<Mapping, GerritNetwork> mappingEventSource =
+ new InformerEventSource<>(
+ InformerConfiguration.from(Mapping.class, context).build(), context);
+
+ Map<String, EventSource> eventSources = new HashMap<>();
+ eventSources.put(MAPPING_EVENT_SOURCE, mappingEventSource);
+ return eventSources;
+ }
+
+ @Override
+ public UpdateControl<GerritNetwork> reconcile(
+ GerritNetwork resource, Context<GerritNetwork> context) throws Exception {
+ return UpdateControl.noUpdate();
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/AbstractAmbassadorDependentResource.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/AbstractAmbassadorDependentResource.java
new file mode 100644
index 0000000..3cc8d4a
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/AbstractAmbassadorDependentResource.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.fabric8.kubernetes.api.model.ObjectMeta;
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+import io.getambassador.v2.MappingSpec;
+import io.getambassador.v2.MappingSpecBuilder;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
+import java.util.List;
+
+public abstract class AbstractAmbassadorDependentResource<T extends HasMetadata>
+ extends CRUDKubernetesDependentResource<T, GerritNetwork> {
+
+ public AbstractAmbassadorDependentResource(Class<T> dependentResourceClass) {
+ super(dependentResourceClass);
+ }
+
+ public ObjectMeta getCommonMetadata(GerritNetwork gerritnetwork, String name, String className) {
+ ObjectMeta metadata =
+ new ObjectMetaBuilder()
+ .withName(name)
+ .withNamespace(gerritnetwork.getMetadata().getNamespace())
+ .withLabels(
+ GerritCluster.getLabels(gerritnetwork.getMetadata().getName(), name, className))
+ .build();
+ return metadata;
+ }
+
+ public MappingSpec getCommonSpec(GerritNetwork gerritnetwork, String serviceName) {
+ MappingSpec spec =
+ new MappingSpecBuilder()
+ .withAmbassadorId(getAmbassadorIds(gerritnetwork))
+ .withHost(gerritnetwork.getSpec().getIngress().getHost())
+ .withPrefix("/")
+ .withService(serviceName)
+ .withBypassAuth(true)
+ .withRewrite("") // important - so the prefix doesn't get overwritten to "/"
+ .build();
+ return spec;
+ }
+
+ public List<String> getAmbassadorIds(GerritNetwork gerritnetwork) {
+ return gerritnetwork.getSpec().getIngress().getAmbassador().getId();
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/CreateHostCondition.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/CreateHostCondition.java
new file mode 100644
index 0000000..e7f99ec
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/CreateHostCondition.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
+
+public class CreateHostCondition implements Condition<Mapping, GerritNetwork> {
+
+ @Override
+ public boolean isMet(
+ DependentResource<Mapping, GerritNetwork> dependentResource,
+ GerritNetwork gerritNetwork,
+ Context<GerritNetwork> context) {
+
+ return gerritNetwork.getSpec().getIngress().isEnabled()
+ && gerritNetwork.getSpec().getIngress().getAmbassador().getCreateHost();
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterHost.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterHost.java
new file mode 100644
index 0000000..5f916c4
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterHost.java
@@ -0,0 +1,62 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterTLSContext.GERRIT_TLS_CONTEXT;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Host;
+import io.getambassador.v2.HostBuilder;
+import io.getambassador.v2.hostspec.TlsContext;
+import io.getambassador.v2.hostspec.TlsSecret;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+
+public class GerritClusterHost extends AbstractAmbassadorDependentResource<Host> {
+
+ public static final String GERRIT_HOST = "gerrit-ambassador-host";
+
+ public GerritClusterHost() {
+ super(Host.class);
+ }
+
+ @Override
+ public Host desired(GerritNetwork gerritNetwork, Context<GerritNetwork> context) {
+
+ TlsSecret tlsSecret = null;
+ TlsContext tlsContext = null;
+
+ if (gerritNetwork.getSpec().getIngress().getTls().isEnabled()) {
+ tlsSecret = new TlsSecret();
+ tlsContext = new TlsContext();
+ tlsSecret.setName(gerritNetwork.getSpec().getIngress().getTls().getSecret());
+ tlsContext.setName(GERRIT_TLS_CONTEXT);
+ }
+
+ Host host =
+ new HostBuilder()
+ .withNewMetadataLike(
+ getCommonMetadata(gerritNetwork, GERRIT_HOST, this.getClass().getSimpleName()))
+ .endMetadata()
+ .withNewSpec()
+ .withAmbassadorId(getAmbassadorIds(gerritNetwork))
+ .withHostname(gerritNetwork.getSpec().getIngress().getHost())
+ .withTlsSecret(tlsSecret)
+ .withTlsContext(tlsContext)
+ .endSpec()
+ .build();
+
+ return host;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMapping.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMapping.java
new file mode 100644
index 0000000..733be73
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMapping.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.NetworkMemberWithSsh;
+import io.getambassador.v2.Mapping;
+import io.getambassador.v2.MappingBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+
+@KubernetesDependent(resourceDiscriminator = GerritClusterMappingDiscriminator.class)
+public class GerritClusterMapping extends AbstractAmbassadorDependentResource<Mapping>
+ implements MappingDependentResourceInterface {
+
+ public static final String GERRIT_MAPPING = "gerrit-mapping";
+
+ public GerritClusterMapping() {
+ super(Mapping.class);
+ }
+
+ @Override
+ public Mapping desired(GerritNetwork gerritNetwork, Context<GerritNetwork> context) {
+
+ // If only one Gerrit instance in GerritCluster, send all git-over-https requests to it
+ NetworkMemberWithSsh gerrit =
+ gerritNetwork.hasGerritReplica()
+ ? gerritNetwork.getSpec().getGerritReplica()
+ : gerritNetwork.getSpec().getPrimaryGerrit();
+ String serviceName = gerrit.getName() + ":" + gerrit.getHttpPort();
+ Mapping mapping =
+ new MappingBuilder()
+ .withNewMetadataLike(
+ getCommonMetadata(gerritNetwork, GERRIT_MAPPING, this.getClass().getSimpleName()))
+ .endMetadata()
+ .withNewSpecLike(getCommonSpec(gerritNetwork, serviceName))
+ .endSpec()
+ .build();
+ return mapping;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingDiscriminator.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingDiscriminator.java
new file mode 100644
index 0000000..12d99c3
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingDiscriminator.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMapping.GERRIT_MAPPING;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.Optional;
+
+public class GerritClusterMappingDiscriminator
+ implements ResourceDiscriminator<Mapping, GerritNetwork> {
+ @Override
+ public Optional<Mapping> distinguish(
+ Class<Mapping> resource, GerritNetwork network, Context<GerritNetwork> context) {
+ InformerEventSource<Mapping, GerritNetwork> ies =
+ (InformerEventSource<Mapping, GerritNetwork>)
+ context.eventSourceRetriever().getResourceEventSourceFor(Mapping.class);
+ return ies.get(new ResourceID(GERRIT_MAPPING, network.getMetadata().getNamespace()));
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingGETReplica.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingGETReplica.java
new file mode 100644
index 0000000..8fda99e
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingGETReplica.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.gerrit.k8s.operator.network.Constants.INFO_REFS_PATTERN;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.getambassador.v2.MappingBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+import java.util.HashMap;
+
+@KubernetesDependent(resourceDiscriminator = GerritClusterMappingGETReplicaDiscriminator.class)
+public class GerritClusterMappingGETReplica extends AbstractAmbassadorDependentResource<Mapping>
+ implements MappingDependentResourceInterface {
+
+ public static final String GERRIT_MAPPING_GET_REPLICA = "gerrit-mapping-get-replica";
+
+ public GerritClusterMappingGETReplica() {
+ super(Mapping.class);
+ }
+
+ @Override
+ public Mapping desired(GerritNetwork gerritNetwork, Context<GerritNetwork> context) {
+
+ String replicaServiceName =
+ gerritNetwork.getSpec().getGerritReplica().getName()
+ + ":"
+ + gerritNetwork.getSpec().getGerritReplica().getHttpPort();
+
+ // Send fetch/clone GET requests to the Replica
+ Mapping mapping =
+ new MappingBuilder()
+ .withNewMetadataLike(
+ getCommonMetadata(
+ gerritNetwork, GERRIT_MAPPING_GET_REPLICA, this.getClass().getSimpleName()))
+ .endMetadata()
+ .withNewSpecLike(getCommonSpec(gerritNetwork, replicaServiceName))
+ .withNewV2QueryParameters()
+ .withAdditionalProperties(
+ new HashMap<String, Object>() {
+ {
+ put("service", "git-upload-pack");
+ }
+ })
+ .endV2QueryParameters()
+ .withMethod("GET")
+ .withPrefix(INFO_REFS_PATTERN)
+ .withPrefixRegex(true)
+ .endSpec()
+ .build();
+
+ return mapping;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingGETReplicaDiscriminator.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingGETReplicaDiscriminator.java
new file mode 100644
index 0000000..9a4da43
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingGETReplicaDiscriminator.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingGETReplica.GERRIT_MAPPING_GET_REPLICA;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.Optional;
+
+public class GerritClusterMappingGETReplicaDiscriminator
+ implements ResourceDiscriminator<Mapping, GerritNetwork> {
+ @Override
+ public Optional<Mapping> distinguish(
+ Class<Mapping> resource, GerritNetwork network, Context<GerritNetwork> context) {
+ InformerEventSource<Mapping, GerritNetwork> ies =
+ (InformerEventSource<Mapping, GerritNetwork>)
+ context.eventSourceRetriever().getResourceEventSourceFor(Mapping.class);
+ return ies.get(
+ new ResourceID(GERRIT_MAPPING_GET_REPLICA, network.getMetadata().getNamespace()));
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPOSTReplica.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPOSTReplica.java
new file mode 100644
index 0000000..1779990
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPOSTReplica.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.gerrit.k8s.operator.network.Constants.UPLOAD_PACK_URL_PATTERN;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.getambassador.v2.MappingBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+
+@KubernetesDependent(resourceDiscriminator = GerritClusterMappingPOSTReplicaDiscriminator.class)
+public class GerritClusterMappingPOSTReplica extends AbstractAmbassadorDependentResource<Mapping>
+ implements MappingDependentResourceInterface {
+
+ public static final String GERRIT_MAPPING_POST_REPLICA = "gerrit-mapping-post-replica";
+
+ public GerritClusterMappingPOSTReplica() {
+ super(Mapping.class);
+ }
+
+ @Override
+ public Mapping desired(GerritNetwork gerritNetwork, Context<GerritNetwork> context) {
+
+ String replicaServiceName =
+ gerritNetwork.getSpec().getGerritReplica().getName()
+ + ":"
+ + gerritNetwork.getSpec().getGerritReplica().getHttpPort();
+
+ // Send fetch/clone POST requests to the Replica
+ Mapping mapping =
+ new MappingBuilder()
+ .withNewMetadataLike(
+ getCommonMetadata(
+ gerritNetwork, GERRIT_MAPPING_POST_REPLICA, this.getClass().getSimpleName()))
+ .endMetadata()
+ .withNewSpecLike(getCommonSpec(gerritNetwork, replicaServiceName))
+ .withPrefix(UPLOAD_PACK_URL_PATTERN)
+ .withPrefixRegex(true)
+ .withMethod("POST")
+ .endSpec()
+ .build();
+ return mapping;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPOSTReplicaDiscriminator.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPOSTReplicaDiscriminator.java
new file mode 100644
index 0000000..a1c02c8
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPOSTReplicaDiscriminator.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingPOSTReplica.GERRIT_MAPPING_POST_REPLICA;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.Optional;
+
+public class GerritClusterMappingPOSTReplicaDiscriminator
+ implements ResourceDiscriminator<Mapping, GerritNetwork> {
+ @Override
+ public Optional<Mapping> distinguish(
+ Class<Mapping> resource, GerritNetwork network, Context<GerritNetwork> context) {
+ InformerEventSource<Mapping, GerritNetwork> ies =
+ (InformerEventSource<Mapping, GerritNetwork>)
+ context.eventSourceRetriever().getResourceEventSourceFor(Mapping.class);
+ return ies.get(
+ new ResourceID(GERRIT_MAPPING_POST_REPLICA, network.getMetadata().getNamespace()));
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPrimary.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPrimary.java
new file mode 100644
index 0000000..f62b74a
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPrimary.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.getambassador.v2.MappingBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+
+@KubernetesDependent(resourceDiscriminator = GerritClusterMappingPrimaryDiscriminator.class)
+public class GerritClusterMappingPrimary extends AbstractAmbassadorDependentResource<Mapping>
+ implements MappingDependentResourceInterface {
+
+ public static final String GERRIT_MAPPING_PRIMARY = "gerrit-mapping-primary";
+
+ public GerritClusterMappingPrimary() {
+ super(Mapping.class);
+ }
+
+ @Override
+ public Mapping desired(GerritNetwork gerritNetwork, Context<GerritNetwork> context) {
+
+ String primaryServiceName =
+ gerritNetwork.getSpec().getPrimaryGerrit().getName()
+ + ":"
+ + gerritNetwork.getSpec().getPrimaryGerrit().getHttpPort();
+
+ // Send all write traffic (non git fetch/clone traffic) to the Primary.
+ // Emissary evaluates more constrained Mappings first.
+ Mapping mapping =
+ new MappingBuilder()
+ .withNewMetadataLike(
+ getCommonMetadata(
+ gerritNetwork, GERRIT_MAPPING_PRIMARY, this.getClass().getSimpleName()))
+ .endMetadata()
+ .withNewSpecLike(getCommonSpec(gerritNetwork, primaryServiceName))
+ .endSpec()
+ .build();
+
+ return mapping;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPrimaryDiscriminator.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPrimaryDiscriminator.java
new file mode 100644
index 0000000..0d38c69
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingPrimaryDiscriminator.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingPrimary.GERRIT_MAPPING_PRIMARY;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.Optional;
+
+public class GerritClusterMappingPrimaryDiscriminator
+ implements ResourceDiscriminator<Mapping, GerritNetwork> {
+ @Override
+ public Optional<Mapping> distinguish(
+ Class<Mapping> resource, GerritNetwork network, Context<GerritNetwork> context) {
+ InformerEventSource<Mapping, GerritNetwork> ies =
+ (InformerEventSource<Mapping, GerritNetwork>)
+ context.eventSourceRetriever().getResourceEventSourceFor(Mapping.class);
+ return ies.get(new ResourceID(GERRIT_MAPPING_PRIMARY, network.getMetadata().getNamespace()));
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiver.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiver.java
new file mode 100644
index 0000000..2d50b65
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiver.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.gerrit.k8s.operator.network.Constants.PROJECTS_URL_PATTERN;
+import static com.google.gerrit.k8s.operator.network.Constants.RECEIVE_PACK_URL_PATTERN;
+
+import com.google.gerrit.k8s.operator.receiver.dependent.ReceiverService;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.getambassador.v2.MappingBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+
+@KubernetesDependent(resourceDiscriminator = GerritClusterMappingReceiverDiscriminator.class)
+public class GerritClusterMappingReceiver extends AbstractAmbassadorDependentResource<Mapping>
+ implements MappingDependentResourceInterface {
+
+ public static final String GERRIT_MAPPING_RECEIVER = "gerrit-mapping-receiver";
+
+ public GerritClusterMappingReceiver() {
+ super(Mapping.class);
+ }
+
+ @Override
+ public Mapping desired(GerritNetwork gerritNetwork, Context<GerritNetwork> context) {
+
+ String receiverServiceName =
+ ReceiverService.getName(gerritNetwork.getSpec().getReceiver().getName())
+ + ":"
+ + gerritNetwork.getSpec().getReceiver().getHttpPort();
+
+ Mapping mapping =
+ new MappingBuilder()
+ .withNewMetadataLike(
+ getCommonMetadata(
+ gerritNetwork, GERRIT_MAPPING_RECEIVER, this.getClass().getSimpleName()))
+ .endMetadata()
+ .withNewSpecLike(getCommonSpec(gerritNetwork, receiverServiceName))
+ .withPrefix(PROJECTS_URL_PATTERN + "|" + RECEIVE_PACK_URL_PATTERN)
+ .withPrefixRegex(true)
+ .endSpec()
+ .build();
+ return mapping;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiverDiscriminator.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiverDiscriminator.java
new file mode 100644
index 0000000..a31e905
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiverDiscriminator.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingReceiver.GERRIT_MAPPING_RECEIVER;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.Optional;
+
+public class GerritClusterMappingReceiverDiscriminator
+ implements ResourceDiscriminator<Mapping, GerritNetwork> {
+ @Override
+ public Optional<Mapping> distinguish(
+ Class<Mapping> resource, GerritNetwork network, Context<GerritNetwork> context) {
+ InformerEventSource<Mapping, GerritNetwork> ies =
+ (InformerEventSource<Mapping, GerritNetwork>)
+ context.eventSourceRetriever().getResourceEventSourceFor(Mapping.class);
+ return ies.get(new ResourceID(GERRIT_MAPPING_RECEIVER, network.getMetadata().getNamespace()));
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiverGET.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiverGET.java
new file mode 100644
index 0000000..aadd9dc
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiverGET.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.gerrit.k8s.operator.network.Constants.INFO_REFS_PATTERN;
+
+import com.google.gerrit.k8s.operator.receiver.dependent.ReceiverService;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.getambassador.v2.MappingBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+import java.util.HashMap;
+
+@KubernetesDependent(resourceDiscriminator = GerritClusterMappingReceiverGETDiscriminator.class)
+public class GerritClusterMappingReceiverGET extends AbstractAmbassadorDependentResource<Mapping>
+ implements MappingDependentResourceInterface {
+
+ public static final String GERRIT_MAPPING_RECEIVER_GET = "gerrit-mapping-receiver-get";
+
+ public GerritClusterMappingReceiverGET() {
+ super(Mapping.class);
+ }
+
+ @Override
+ public Mapping desired(GerritNetwork gerritNetwork, Context<GerritNetwork> context) {
+
+ String receiverServiceName =
+ ReceiverService.getName(gerritNetwork.getSpec().getReceiver().getName())
+ + ":"
+ + gerritNetwork.getSpec().getReceiver().getHttpPort();
+
+ Mapping mapping =
+ new MappingBuilder()
+ .withNewMetadataLike(
+ getCommonMetadata(
+ gerritNetwork, GERRIT_MAPPING_RECEIVER_GET, this.getClass().getSimpleName()))
+ .endMetadata()
+ .withNewSpecLike(getCommonSpec(gerritNetwork, receiverServiceName))
+ .withNewV2QueryParameters()
+ .withAdditionalProperties(
+ new HashMap<String, Object>() {
+ {
+ put("service", "git-receive-pack");
+ }
+ })
+ .endV2QueryParameters()
+ .withMethod("GET")
+ .withPrefix(INFO_REFS_PATTERN)
+ .withPrefixRegex(true)
+ .endSpec()
+ .build();
+ return mapping;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiverGETDiscriminator.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiverGETDiscriminator.java
new file mode 100644
index 0000000..24cf7f8
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterMappingReceiverGETDiscriminator.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.gerrit.k8s.operator.network.ambassador.dependent.GerritClusterMappingReceiverGET.GERRIT_MAPPING_RECEIVER_GET;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.Optional;
+
+public class GerritClusterMappingReceiverGETDiscriminator
+ implements ResourceDiscriminator<Mapping, GerritNetwork> {
+ @Override
+ public Optional<Mapping> distinguish(
+ Class<Mapping> resource, GerritNetwork network, Context<GerritNetwork> context) {
+ InformerEventSource<Mapping, GerritNetwork> ies =
+ (InformerEventSource<Mapping, GerritNetwork>)
+ context.eventSourceRetriever().getResourceEventSourceFor(Mapping.class);
+ return ies.get(
+ new ResourceID(GERRIT_MAPPING_RECEIVER_GET, network.getMetadata().getNamespace()));
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterTLSContext.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterTLSContext.java
new file mode 100644
index 0000000..7cae8da
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterTLSContext.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.TLSContext;
+import io.getambassador.v2.TLSContextBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import java.util.List;
+
+public class GerritClusterTLSContext extends AbstractAmbassadorDependentResource<TLSContext> {
+
+ public static final String GERRIT_TLS_CONTEXT = "gerrit-tls-context";
+
+ public GerritClusterTLSContext() {
+ super(TLSContext.class);
+ }
+
+ @Override
+ protected TLSContext desired(GerritNetwork gerritNetwork, Context<GerritNetwork> context) {
+ TLSContext tlsContext =
+ new TLSContextBuilder()
+ .withNewMetadataLike(
+ getCommonMetadata(
+ gerritNetwork, GERRIT_TLS_CONTEXT, this.getClass().getSimpleName()))
+ .endMetadata()
+ .withNewSpec()
+ .withAmbassadorId(getAmbassadorIds(gerritNetwork))
+ .withSecret(gerritNetwork.getSpec().getIngress().getTls().getSecret())
+ .withHosts(List.of(gerritNetwork.getSpec().getIngress().getHost()))
+ .withSecretNamespacing(true)
+ .endSpec()
+ .build();
+ return tlsContext;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/LoadBalanceCondition.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/LoadBalanceCondition.java
new file mode 100644
index 0000000..ea9066a
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/LoadBalanceCondition.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
+
+public class LoadBalanceCondition implements Condition<Mapping, GerritNetwork> {
+
+ @Override
+ public boolean isMet(
+ DependentResource<Mapping, GerritNetwork> dependentResource,
+ GerritNetwork gerritNetwork,
+ Context<GerritNetwork> context) {
+
+ return gerritNetwork.getSpec().getIngress().isEnabled()
+ && gerritNetwork.hasPrimaryGerrit()
+ && gerritNetwork.hasGerritReplica();
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/MappingDependentResourceInterface.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/MappingDependentResourceInterface.java
new file mode 100644
index 0000000..dc27428
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/MappingDependentResourceInterface.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+
+public interface MappingDependentResourceInterface {
+ public Mapping desired(GerritNetwork gerritNetwork, Context<GerritNetwork> context);
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/ReceiverMappingCondition.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/ReceiverMappingCondition.java
new file mode 100644
index 0000000..a83f984
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/ReceiverMappingCondition.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
+
+public class ReceiverMappingCondition implements Condition<Mapping, GerritNetwork> {
+
+ @Override
+ public boolean isMet(
+ DependentResource<Mapping, GerritNetwork> dependentResource,
+ GerritNetwork gerritNetwork,
+ Context<GerritNetwork> context) {
+
+ return gerritNetwork.getSpec().getIngress().isEnabled() && gerritNetwork.hasReceiver();
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/SingleMappingCondition.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/SingleMappingCondition.java
new file mode 100644
index 0000000..52a1f2c
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/SingleMappingCondition.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
+
+public class SingleMappingCondition implements Condition<Mapping, GerritNetwork> {
+
+ @Override
+ public boolean isMet(
+ DependentResource<Mapping, GerritNetwork> dependentResource,
+ GerritNetwork gerritNetwork,
+ Context<GerritNetwork> context) {
+
+ return gerritNetwork.getSpec().getIngress().isEnabled()
+ && (gerritNetwork.hasPrimaryGerrit() ^ gerritNetwork.hasGerritReplica());
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/TLSContextCondition.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/TLSContextCondition.java
new file mode 100644
index 0000000..053c559
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/TLSContextCondition.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Mapping;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
+
+public class TLSContextCondition implements Condition<Mapping, GerritNetwork> {
+
+ @Override
+ public boolean isMet(
+ DependentResource<Mapping, GerritNetwork> dependentResource,
+ GerritNetwork gerritNetwork,
+ Context<GerritNetwork> context) {
+
+ return gerritNetwork.getSpec().getIngress().isEnabled()
+ && gerritNetwork.getSpec().getIngress().getTls().isEnabled();
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ingress/GerritIngressReconciler.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ingress/GerritIngressReconciler.java
new file mode 100644
index 0000000..30c90a9
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ingress/GerritIngressReconciler.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ingress;
+
+import com.google.gerrit.k8s.operator.network.GerritClusterIngressCondition;
+import com.google.gerrit.k8s.operator.network.ingress.dependent.GerritClusterIngress;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import com.google.inject.Singleton;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
+import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
+
+@Singleton
+@ControllerConfiguration(
+ dependents = {
+ @Dependent(
+ name = "gerrit-ingress",
+ type = GerritClusterIngress.class,
+ reconcilePrecondition = GerritClusterIngressCondition.class)
+ })
+public class GerritIngressReconciler implements Reconciler<GerritNetwork> {
+
+ @Override
+ public UpdateControl<GerritNetwork> reconcile(
+ GerritNetwork resource, Context<GerritNetwork> context) throws Exception {
+ return UpdateControl.noUpdate();
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ingress/dependent/GerritClusterIngress.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ingress/dependent/GerritClusterIngress.java
new file mode 100644
index 0000000..a62e298
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/ingress/dependent/GerritClusterIngress.java
@@ -0,0 +1,247 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ingress.dependent;
+
+import static com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork.SESSION_COOKIE_NAME;
+
+import com.google.gerrit.k8s.operator.gerrit.dependent.GerritService;
+import com.google.gerrit.k8s.operator.receiver.dependent.ReceiverService;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.fabric8.kubernetes.api.model.networking.v1.HTTPIngressPath;
+import io.fabric8.kubernetes.api.model.networking.v1.HTTPIngressPathBuilder;
+import io.fabric8.kubernetes.api.model.networking.v1.Ingress;
+import io.fabric8.kubernetes.api.model.networking.v1.IngressBuilder;
+import io.fabric8.kubernetes.api.model.networking.v1.IngressRule;
+import io.fabric8.kubernetes.api.model.networking.v1.IngressRuleBuilder;
+import io.fabric8.kubernetes.api.model.networking.v1.IngressSpecBuilder;
+import io.fabric8.kubernetes.api.model.networking.v1.IngressTLS;
+import io.fabric8.kubernetes.api.model.networking.v1.IngressTLSBuilder;
+import io.fabric8.kubernetes.api.model.networking.v1.ServiceBackendPort;
+import io.fabric8.kubernetes.api.model.networking.v1.ServiceBackendPortBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@KubernetesDependent
+public class GerritClusterIngress extends CRUDKubernetesDependentResource<Ingress, GerritNetwork> {
+ private static final String UPLOAD_PACK_URL_PATTERN = "/.*/git-upload-pack";
+ private static final String RECEIVE_PACK_URL_PATTERN = "/.*/git-receive-pack";
+ public static final String INGRESS_NAME = "gerrit-ingress";
+
+ public GerritClusterIngress() {
+ super(Ingress.class);
+ }
+
+ @Override
+ protected Ingress desired(GerritNetwork gerritNetwork, Context<GerritNetwork> context) {
+ IngressSpecBuilder ingressSpecBuilder =
+ new IngressSpecBuilder().withRules(getIngressRule(gerritNetwork));
+ if (gerritNetwork.getSpec().getIngress().getTls().isEnabled()) {
+ ingressSpecBuilder.withTls(getIngressTLS(gerritNetwork));
+ }
+
+ Ingress gerritIngress =
+ new IngressBuilder()
+ .withNewMetadata()
+ .withName("gerrit-ingress")
+ .withNamespace(gerritNetwork.getMetadata().getNamespace())
+ .withLabels(
+ GerritCluster.getLabels(
+ gerritNetwork.getMetadata().getName(),
+ "gerrit-ingress",
+ this.getClass().getSimpleName()))
+ .withAnnotations(getAnnotations(gerritNetwork))
+ .endMetadata()
+ .withSpec(ingressSpecBuilder.build())
+ .build();
+
+ return gerritIngress;
+ }
+
+ private Map<String, String> getAnnotations(GerritNetwork gerritNetwork) {
+ Map<String, String> annotations = gerritNetwork.getSpec().getIngress().getAnnotations();
+ if (annotations == null) {
+ annotations = new HashMap<>();
+ }
+ annotations.put("nginx.ingress.kubernetes.io/use-regex", "true");
+ annotations.put("kubernetes.io/ingress.class", "nginx");
+
+ String configSnippet = "";
+ if (gerritNetwork.hasPrimaryGerrit() && gerritNetwork.hasGerritReplica()) {
+ String svcName = GerritService.getName(gerritNetwork.getSpec().getGerritReplica().getName());
+ configSnippet =
+ createNginxConfigSnippet(
+ "service=git-upload-pack", gerritNetwork.getMetadata().getNamespace(), svcName);
+ }
+ if (gerritNetwork.hasReceiver()) {
+ String svcName = ReceiverService.getName(gerritNetwork.getSpec().getReceiver().getName());
+ configSnippet =
+ createNginxConfigSnippet(
+ "service=git-receive-pack", gerritNetwork.getMetadata().getNamespace(), svcName);
+ }
+ if (!configSnippet.isBlank()) {
+ annotations.put("nginx.ingress.kubernetes.io/configuration-snippet", configSnippet);
+ }
+
+ annotations.put("nginx.ingress.kubernetes.io/affinity", "cookie");
+ annotations.put("nginx.ingress.kubernetes.io/session-cookie-name", SESSION_COOKIE_NAME);
+ annotations.put("nginx.ingress.kubernetes.io/session-cookie-path", "/");
+ annotations.put("nginx.ingress.kubernetes.io/session-cookie-max-age", "60");
+ annotations.put("nginx.ingress.kubernetes.io/session-cookie-expires", "60");
+
+ return annotations;
+ }
+
+ /**
+ * Creates a config snippet for the Nginx Ingress Controller [1]. This snippet will configure
+ * Nginx to route the request based on the `service` query parameter.
+ *
+ * <p>If it is set to `git-upload-pack` it will route the request to the provided service.
+ *
+ * <p>[1]https://docs.nginx.com/nginx-ingress-controller/configuration/ingress-resources/advanced-configuration-with-snippets/
+ *
+ * @param namespace Namespace of the destination service.
+ * @param svcName Name of the destination service.
+ * @return configuration snippet
+ */
+ private String createNginxConfigSnippet(String queryParam, String namespace, String svcName) {
+ StringBuilder configSnippet = new StringBuilder();
+ configSnippet.append("if ($args ~ ");
+ configSnippet.append(queryParam);
+ configSnippet.append("){");
+ configSnippet.append("\n");
+ configSnippet.append(" set $proxy_upstream_name \"");
+ configSnippet.append(namespace);
+ configSnippet.append("-");
+ configSnippet.append(svcName);
+ configSnippet.append("-");
+ configSnippet.append(GerritService.HTTP_PORT_NAME);
+ configSnippet.append("\";\n");
+ configSnippet.append(" set $proxy_host $proxy_upstream_name;");
+ configSnippet.append("\n");
+ configSnippet.append(" set $service_name \"");
+ configSnippet.append(svcName);
+ configSnippet.append("\";\n}");
+ return configSnippet.toString();
+ }
+
+ private IngressTLS getIngressTLS(GerritNetwork gerritNetwork) {
+ if (gerritNetwork.getSpec().getIngress().getTls().isEnabled()) {
+ return new IngressTLSBuilder()
+ .withHosts(gerritNetwork.getSpec().getIngress().getHost())
+ .withSecretName(gerritNetwork.getSpec().getIngress().getTls().getSecret())
+ .build();
+ }
+ return null;
+ }
+
+ private IngressRule getIngressRule(GerritNetwork gerritNetwork) {
+ List<HTTPIngressPath> ingressPaths = new ArrayList<>();
+ if (gerritNetwork.hasReceiver()) {
+ ingressPaths.addAll(getReceiverIngressPaths(gerritNetwork));
+ }
+ if (gerritNetwork.hasGerrits()) {
+ ingressPaths.addAll(getGerritHTTPIngressPaths(gerritNetwork));
+ }
+
+ if (ingressPaths.isEmpty()) {
+ throw new IllegalStateException(
+ "Failed to create Ingress: No Receiver or Gerrit in GerritCluster.");
+ }
+
+ return new IngressRuleBuilder()
+ .withHost(gerritNetwork.getSpec().getIngress().getHost())
+ .withNewHttp()
+ .withPaths(ingressPaths)
+ .endHttp()
+ .build();
+ }
+
+ private List<HTTPIngressPath> getGerritHTTPIngressPaths(GerritNetwork gerritNetwork) {
+ ServiceBackendPort port =
+ new ServiceBackendPortBuilder().withName(GerritService.HTTP_PORT_NAME).build();
+
+ List<HTTPIngressPath> paths = new ArrayList<>();
+ // Order matters, since routing rules will be applied in order!
+ if (!gerritNetwork.hasPrimaryGerrit() && gerritNetwork.hasGerritReplica()) {
+ paths.add(
+ new HTTPIngressPathBuilder()
+ .withPathType("Prefix")
+ .withPath("/")
+ .withNewBackend()
+ .withNewService()
+ .withName(GerritService.getName(gerritNetwork.getSpec().getGerritReplica().getName()))
+ .withPort(port)
+ .endService()
+ .endBackend()
+ .build());
+ return paths;
+ }
+ if (gerritNetwork.hasGerritReplica()) {
+ paths.add(
+ new HTTPIngressPathBuilder()
+ .withPathType("Prefix")
+ .withPath(UPLOAD_PACK_URL_PATTERN)
+ .withNewBackend()
+ .withNewService()
+ .withName(GerritService.getName(gerritNetwork.getSpec().getGerritReplica().getName()))
+ .withPort(port)
+ .endService()
+ .endBackend()
+ .build());
+ }
+ if (gerritNetwork.hasPrimaryGerrit()) {
+ paths.add(
+ new HTTPIngressPathBuilder()
+ .withPathType("Prefix")
+ .withPath("/")
+ .withNewBackend()
+ .withNewService()
+ .withName(GerritService.getName(gerritNetwork.getSpec().getPrimaryGerrit().getName()))
+ .withPort(port)
+ .endService()
+ .endBackend()
+ .build());
+ }
+ return paths;
+ }
+
+ private List<HTTPIngressPath> getReceiverIngressPaths(GerritNetwork gerritNetwork) {
+ String svcName = ReceiverService.getName(gerritNetwork.getSpec().getReceiver().getName());
+ List<HTTPIngressPath> paths = new ArrayList<>();
+ ServiceBackendPort port =
+ new ServiceBackendPortBuilder().withName(ReceiverService.HTTP_PORT_NAME).build();
+
+ for (String path : List.of("/a/projects", RECEIVE_PACK_URL_PATTERN)) {
+ paths.add(
+ new HTTPIngressPathBuilder()
+ .withPathType("Prefix")
+ .withPath(path)
+ .withNewBackend()
+ .withNewService()
+ .withName(svcName)
+ .withPort(port)
+ .endService()
+ .endBackend()
+ .build());
+ }
+ return paths;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/GerritIstioReconciler.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/GerritIstioReconciler.java
new file mode 100644
index 0000000..4d12941
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/GerritIstioReconciler.java
@@ -0,0 +1,89 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.istio;
+
+import static com.google.gerrit.k8s.operator.network.istio.GerritIstioReconciler.ISTIO_DESTINATION_RULE_EVENT_SOURCE;
+import static com.google.gerrit.k8s.operator.network.istio.GerritIstioReconciler.ISTIO_VIRTUAL_SERVICE_EVENT_SOURCE;
+
+import com.google.gerrit.k8s.operator.network.GerritClusterIngressCondition;
+import com.google.gerrit.k8s.operator.network.istio.dependent.GerritClusterIstioGateway;
+import com.google.gerrit.k8s.operator.network.istio.dependent.GerritIstioCondition;
+import com.google.gerrit.k8s.operator.network.istio.dependent.GerritIstioDestinationRule;
+import com.google.gerrit.k8s.operator.network.istio.dependent.GerritIstioVirtualService;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import com.google.inject.Singleton;
+import io.fabric8.istio.api.networking.v1beta1.DestinationRule;
+import io.fabric8.istio.api.networking.v1beta1.VirtualService;
+import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
+import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer;
+import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
+import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
+import io.javaoperatorsdk.operator.processing.event.source.EventSource;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.HashMap;
+import java.util.Map;
+
+@Singleton
+@ControllerConfiguration(
+ dependents = {
+ @Dependent(
+ name = "gerrit-destination-rules",
+ type = GerritIstioDestinationRule.class,
+ reconcilePrecondition = GerritIstioCondition.class,
+ useEventSourceWithName = ISTIO_DESTINATION_RULE_EVENT_SOURCE),
+ @Dependent(
+ name = "gerrit-istio-gateway",
+ type = GerritClusterIstioGateway.class,
+ reconcilePrecondition = GerritClusterIngressCondition.class),
+ @Dependent(
+ name = "gerrit-istio-virtual-service",
+ type = GerritIstioVirtualService.class,
+ reconcilePrecondition = GerritIstioCondition.class,
+ dependsOn = {"gerrit-istio-gateway"},
+ useEventSourceWithName = ISTIO_VIRTUAL_SERVICE_EVENT_SOURCE),
+ })
+public class GerritIstioReconciler
+ implements Reconciler<GerritNetwork>, EventSourceInitializer<GerritNetwork> {
+ public static final String ISTIO_DESTINATION_RULE_EVENT_SOURCE =
+ "gerrit-cluster-istio-destination-rule";
+ public static final String ISTIO_VIRTUAL_SERVICE_EVENT_SOURCE =
+ "gerrit-cluster-istio-virtual-service";
+
+ @Override
+ public Map<String, EventSource> prepareEventSources(EventSourceContext<GerritNetwork> context) {
+ InformerEventSource<DestinationRule, GerritNetwork> gerritIstioDestinationRuleEventSource =
+ new InformerEventSource<>(
+ InformerConfiguration.from(DestinationRule.class, context).build(), context);
+
+ InformerEventSource<VirtualService, GerritNetwork> virtualServiceEventSource =
+ new InformerEventSource<>(
+ InformerConfiguration.from(VirtualService.class, context).build(), context);
+
+ Map<String, EventSource> eventSources = new HashMap<>();
+ eventSources.put(ISTIO_DESTINATION_RULE_EVENT_SOURCE, gerritIstioDestinationRuleEventSource);
+ eventSources.put(ISTIO_VIRTUAL_SERVICE_EVENT_SOURCE, virtualServiceEventSource);
+ return eventSources;
+ }
+
+ @Override
+ public UpdateControl<GerritNetwork> reconcile(
+ GerritNetwork resource, Context<GerritNetwork> context) throws Exception {
+ return UpdateControl.noUpdate();
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritClusterIstioGateway.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritClusterIstioGateway.java
new file mode 100644
index 0000000..099a4ed
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritClusterIstioGateway.java
@@ -0,0 +1,115 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.istio.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.fabric8.istio.api.networking.v1beta1.Gateway;
+import io.fabric8.istio.api.networking.v1beta1.GatewayBuilder;
+import io.fabric8.istio.api.networking.v1beta1.Server;
+import io.fabric8.istio.api.networking.v1beta1.ServerBuilder;
+import io.fabric8.istio.api.networking.v1beta1.ServerTLSSettingsTLSmode;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class GerritClusterIstioGateway
+ extends CRUDKubernetesDependentResource<Gateway, GerritNetwork> {
+ public static final String NAME = "gerrit-istio-gateway";
+
+ public GerritClusterIstioGateway() {
+ super(Gateway.class);
+ }
+
+ @Override
+ public Gateway desired(GerritNetwork gerritNetwork, Context<GerritNetwork> context) {
+ return new GatewayBuilder()
+ .withNewMetadata()
+ .withName(NAME)
+ .withNamespace(gerritNetwork.getMetadata().getNamespace())
+ .withLabels(
+ GerritCluster.getLabels(
+ gerritNetwork.getMetadata().getName(), NAME, this.getClass().getSimpleName()))
+ .endMetadata()
+ .withNewSpec()
+ .withSelector(Map.of("istio", "ingressgateway"))
+ .withServers(configureServers(gerritNetwork))
+ .endSpec()
+ .build();
+ }
+
+ private List<Server> configureServers(GerritNetwork gerritNetwork) {
+ List<Server> servers = new ArrayList<>();
+ String gerritClusterHost = gerritNetwork.getSpec().getIngress().getHost();
+
+ servers.add(
+ new ServerBuilder()
+ .withNewPort()
+ .withName("http")
+ .withNumber(80)
+ .withProtocol("HTTP")
+ .endPort()
+ .withHosts(gerritClusterHost)
+ .withNewTls()
+ .withHttpsRedirect(gerritNetwork.getSpec().getIngress().getTls().isEnabled())
+ .endTls()
+ .build());
+
+ if (gerritNetwork.getSpec().getIngress().getTls().isEnabled()) {
+ servers.add(
+ new ServerBuilder()
+ .withNewPort()
+ .withName("https")
+ .withNumber(443)
+ .withProtocol("HTTPS")
+ .endPort()
+ .withHosts(gerritClusterHost)
+ .withNewTls()
+ .withMode(ServerTLSSettingsTLSmode.SIMPLE)
+ .withCredentialName(gerritNetwork.getSpec().getIngress().getTls().getSecret())
+ .endTls()
+ .build());
+ }
+
+ if (gerritNetwork.getSpec().getIngress().getSsh().isEnabled() && gerritNetwork.hasGerrits()) {
+ if (gerritNetwork.hasPrimaryGerrit()) {
+ servers.add(
+ new ServerBuilder()
+ .withNewPort()
+ .withName("ssh-primary")
+ .withNumber(gerritNetwork.getSpec().getPrimaryGerrit().getSshPort())
+ .withProtocol("TCP")
+ .endPort()
+ .withHosts(gerritClusterHost)
+ .build());
+ }
+ if (gerritNetwork.hasGerritReplica()) {
+ servers.add(
+ new ServerBuilder()
+ .withNewPort()
+ .withName("ssh-replica")
+ .withNumber(gerritNetwork.getSpec().getGerritReplica().getSshPort())
+ .withProtocol("TCP")
+ .endPort()
+ .withHosts(gerritClusterHost)
+ .build());
+ }
+ }
+
+ return servers;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritIstioCondition.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritIstioCondition.java
new file mode 100644
index 0000000..ac6c6c3
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritIstioCondition.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.istio.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.fabric8.istio.api.networking.v1beta1.VirtualService;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
+
+public class GerritIstioCondition implements Condition<VirtualService, GerritNetwork> {
+
+ @Override
+ public boolean isMet(
+ DependentResource<VirtualService, GerritNetwork> dependentResource,
+ GerritNetwork gerritNetwork,
+ Context<GerritNetwork> context) {
+
+ return gerritNetwork.getSpec().getIngress().isEnabled()
+ && (gerritNetwork.hasGerrits() || gerritNetwork.hasReceiver());
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritIstioDestinationRule.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritIstioDestinationRule.java
new file mode 100644
index 0000000..98f8267
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritIstioDestinationRule.java
@@ -0,0 +1,131 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.istio.dependent;
+
+import static com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork.SESSION_COOKIE_NAME;
+import static com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork.SESSION_COOKIE_TTL;
+
+import com.google.gerrit.k8s.operator.gerrit.dependent.GerritService;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplate;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.fabric8.istio.api.networking.v1beta1.DestinationRule;
+import io.fabric8.istio.api.networking.v1beta1.DestinationRuleBuilder;
+import io.fabric8.istio.api.networking.v1beta1.LoadBalancerSettingsSimpleLB;
+import io.fabric8.istio.api.networking.v1beta1.TrafficPolicy;
+import io.fabric8.istio.api.networking.v1beta1.TrafficPolicyBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected;
+import io.javaoperatorsdk.operator.processing.dependent.BulkDependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.Creator;
+import io.javaoperatorsdk.operator.processing.dependent.Updater;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+public class GerritIstioDestinationRule
+ extends KubernetesDependentResource<DestinationRule, GerritNetwork>
+ implements Creator<DestinationRule, GerritNetwork>,
+ Updater<DestinationRule, GerritNetwork>,
+ Deleter<GerritNetwork>,
+ BulkDependentResource<DestinationRule, GerritNetwork>,
+ GarbageCollected<GerritNetwork> {
+
+ public GerritIstioDestinationRule() {
+ super(DestinationRule.class);
+ }
+
+ protected DestinationRule desired(
+ GerritNetwork gerritNetwork, String gerritName, boolean isReplica) {
+
+ return new DestinationRuleBuilder()
+ .withNewMetadata()
+ .withName(getName(gerritName))
+ .withNamespace(gerritNetwork.getMetadata().getNamespace())
+ .withLabels(
+ GerritCluster.getLabels(
+ gerritNetwork.getMetadata().getName(),
+ getName(gerritName),
+ this.getClass().getSimpleName()))
+ .endMetadata()
+ .withNewSpec()
+ .withHost(GerritService.getHostname(gerritName, gerritNetwork.getMetadata().getNamespace()))
+ .withTrafficPolicy(getTrafficPolicy(isReplica))
+ .endSpec()
+ .build();
+ }
+
+ private TrafficPolicy getTrafficPolicy(boolean isReplica) {
+ if (isReplica) {
+ return new TrafficPolicyBuilder()
+ .withNewLoadBalancer()
+ .withNewLoadBalancerSettingsSimpleLbPolicy()
+ .withSimple(LoadBalancerSettingsSimpleLB.LEAST_CONN)
+ .endLoadBalancerSettingsSimpleLbPolicy()
+ .endLoadBalancer()
+ .build();
+ }
+ return new TrafficPolicyBuilder()
+ .withNewLoadBalancer()
+ .withNewLoadBalancerSettingsConsistentHashLbPolicy()
+ .withNewConsistentHash()
+ .withNewLoadBalancerSettingsConsistentHashLBHttpCookieKey()
+ .withNewHttpCookie()
+ .withName(SESSION_COOKIE_NAME)
+ .withTtl(SESSION_COOKIE_TTL)
+ .endHttpCookie()
+ .endLoadBalancerSettingsConsistentHashLBHttpCookieKey()
+ .endConsistentHash()
+ .endLoadBalancerSettingsConsistentHashLbPolicy()
+ .endLoadBalancer()
+ .build();
+ }
+
+ public static String getName(GerritTemplate gerrit) {
+ return gerrit.getMetadata().getName();
+ }
+
+ public static String getName(String gerritName) {
+ return gerritName;
+ }
+
+ @Override
+ public Map<String, DestinationRule> desiredResources(
+ GerritNetwork gerritNetwork, Context<GerritNetwork> context) {
+ Map<String, DestinationRule> drs = new HashMap<>();
+ if (gerritNetwork.hasPrimaryGerrit()) {
+ String primaryGerritName = gerritNetwork.getSpec().getPrimaryGerrit().getName();
+ drs.put(primaryGerritName, desired(gerritNetwork, primaryGerritName, false));
+ }
+ if (gerritNetwork.hasGerritReplica()) {
+ String gerritReplicaName = gerritNetwork.getSpec().getGerritReplica().getName();
+ drs.put(gerritReplicaName, desired(gerritNetwork, gerritReplicaName, true));
+ }
+ return drs;
+ }
+
+ @Override
+ public Map<String, DestinationRule> getSecondaryResources(
+ GerritNetwork gerritNetwork, Context<GerritNetwork> context) {
+ Set<DestinationRule> drs = context.getSecondaryResources(DestinationRule.class);
+ Map<String, DestinationRule> result = new HashMap<>(drs.size());
+ for (DestinationRule dr : drs) {
+ result.put(dr.getMetadata().getName(), dr);
+ }
+ return result;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritIstioVirtualService.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritIstioVirtualService.java
new file mode 100644
index 0000000..f0d683c
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritIstioVirtualService.java
@@ -0,0 +1,238 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.istio.dependent;
+
+import com.google.gerrit.k8s.operator.gerrit.dependent.GerritService;
+import com.google.gerrit.k8s.operator.receiver.dependent.ReceiverService;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.NetworkMember;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.NetworkMemberWithSsh;
+import io.fabric8.istio.api.networking.v1beta1.HTTPMatchRequest;
+import io.fabric8.istio.api.networking.v1beta1.HTTPMatchRequestBuilder;
+import io.fabric8.istio.api.networking.v1beta1.HTTPRoute;
+import io.fabric8.istio.api.networking.v1beta1.HTTPRouteBuilder;
+import io.fabric8.istio.api.networking.v1beta1.HTTPRouteDestination;
+import io.fabric8.istio.api.networking.v1beta1.HTTPRouteDestinationBuilder;
+import io.fabric8.istio.api.networking.v1beta1.L4MatchAttributesBuilder;
+import io.fabric8.istio.api.networking.v1beta1.RouteDestination;
+import io.fabric8.istio.api.networking.v1beta1.RouteDestinationBuilder;
+import io.fabric8.istio.api.networking.v1beta1.StringMatchBuilder;
+import io.fabric8.istio.api.networking.v1beta1.TCPRoute;
+import io.fabric8.istio.api.networking.v1beta1.TCPRouteBuilder;
+import io.fabric8.istio.api.networking.v1beta1.VirtualService;
+import io.fabric8.istio.api.networking.v1beta1.VirtualServiceBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+@KubernetesDependent
+public class GerritIstioVirtualService
+ extends CRUDKubernetesDependentResource<VirtualService, GerritNetwork> {
+ private static final String INFO_REF_URL_PATTERN = "^/(.*)/info/refs$";
+ private static final String UPLOAD_PACK_URL_PATTERN = "^/(.*)/git-upload-pack$";
+ private static final String RECEIVE_PACK_URL_PATTERN = "^/(.*)/git-receive-pack$";
+ public static final String NAME_SUFFIX = "gerrit-http-virtual-service";
+
+ public GerritIstioVirtualService() {
+ super(VirtualService.class);
+ }
+
+ @Override
+ protected VirtualService desired(GerritNetwork gerritNetwork, Context<GerritNetwork> context) {
+ String gerritClusterHost = gerritNetwork.getSpec().getIngress().getHost();
+ String namespace = gerritNetwork.getMetadata().getNamespace();
+
+ return new VirtualServiceBuilder()
+ .withNewMetadata()
+ .withName(gerritNetwork.getDependentResourceName(NAME_SUFFIX))
+ .withNamespace(namespace)
+ .withLabels(
+ GerritCluster.getLabels(
+ gerritNetwork.getMetadata().getName(),
+ gerritNetwork.getDependentResourceName(NAME_SUFFIX),
+ this.getClass().getSimpleName()))
+ .endMetadata()
+ .withNewSpec()
+ .withHosts(gerritClusterHost)
+ .withGateways(namespace + "/" + GerritClusterIstioGateway.NAME)
+ .withHttp(getHTTPRoutes(gerritNetwork))
+ .withTcp(getTCPRoutes(gerritNetwork))
+ .endSpec()
+ .build();
+ }
+
+ private List<HTTPRoute> getHTTPRoutes(GerritNetwork gerritNetwork) {
+ String namespace = gerritNetwork.getMetadata().getNamespace();
+ List<HTTPRoute> routes = new ArrayList<>();
+ if (gerritNetwork.hasReceiver()) {
+ routes.add(
+ new HTTPRouteBuilder()
+ .withName("receiver-" + gerritNetwork.getSpec().getReceiver().getName())
+ .withMatch(getReceiverMatches())
+ .withRoute(
+ getReceiverHTTPDestination(gerritNetwork.getSpec().getReceiver(), namespace))
+ .build());
+ }
+ if (gerritNetwork.hasGerritReplica()) {
+ HTTPRouteBuilder routeBuilder =
+ new HTTPRouteBuilder()
+ .withName("gerrit-replica-" + gerritNetwork.getSpec().getGerritReplica().getName());
+ if (gerritNetwork.hasPrimaryGerrit()) {
+ routeBuilder = routeBuilder.withMatch(getGerritReplicaMatches());
+ }
+ routes.add(
+ routeBuilder
+ .withRoute(
+ getGerritHTTPDestinations(gerritNetwork.getSpec().getGerritReplica(), namespace))
+ .build());
+ }
+ if (gerritNetwork.hasPrimaryGerrit()) {
+ routes.add(
+ new HTTPRouteBuilder()
+ .withName("gerrit-primary-" + gerritNetwork.getSpec().getPrimaryGerrit().getName())
+ .withRoute(
+ getGerritHTTPDestinations(gerritNetwork.getSpec().getPrimaryGerrit(), namespace))
+ .build());
+ }
+
+ return routes;
+ }
+
+ private HTTPRouteDestination getGerritHTTPDestinations(
+ NetworkMemberWithSsh networkMember, String namespace) {
+ return new HTTPRouteDestinationBuilder()
+ .withNewDestination()
+ .withHost(GerritService.getHostname(networkMember.getName(), namespace))
+ .withNewPort()
+ .withNumber(networkMember.getHttpPort())
+ .endPort()
+ .endDestination()
+ .build();
+ }
+
+ private List<HTTPMatchRequest> getGerritReplicaMatches() {
+ List<HTTPMatchRequest> matches = new ArrayList<>();
+ matches.add(
+ new HTTPMatchRequestBuilder()
+ .withNewUri()
+ .withNewStringMatchRegexType()
+ .withRegex(INFO_REF_URL_PATTERN)
+ .endStringMatchRegexType()
+ .endUri()
+ .withQueryParams(
+ Map.of(
+ "service",
+ new StringMatchBuilder()
+ .withNewStringMatchExactType("git-upload-pack")
+ .build()))
+ .withIgnoreUriCase()
+ .withNewMethod()
+ .withNewStringMatchExactType()
+ .withExact("GET")
+ .endStringMatchExactType()
+ .endMethod()
+ .build());
+ matches.add(
+ new HTTPMatchRequestBuilder()
+ .withNewUri()
+ .withNewStringMatchRegexType()
+ .withRegex(UPLOAD_PACK_URL_PATTERN)
+ .endStringMatchRegexType()
+ .endUri()
+ .withIgnoreUriCase()
+ .withNewMethod()
+ .withNewStringMatchExactType()
+ .withExact("POST")
+ .endStringMatchExactType()
+ .endMethod()
+ .build());
+ return matches;
+ }
+
+ private HTTPRouteDestination getReceiverHTTPDestination(
+ NetworkMember receiver, String namespace) {
+ return new HTTPRouteDestinationBuilder()
+ .withNewDestination()
+ .withHost(ReceiverService.getHostname(receiver.getName(), namespace))
+ .withNewPort()
+ .withNumber(receiver.getHttpPort())
+ .endPort()
+ .endDestination()
+ .build();
+ }
+
+ private List<HTTPMatchRequest> getReceiverMatches() {
+ List<HTTPMatchRequest> matches = new ArrayList<>();
+ matches.add(
+ new HTTPMatchRequestBuilder()
+ .withUri(new StringMatchBuilder().withNewStringMatchPrefixType("/a/projects/").build())
+ .build());
+ matches.add(
+ new HTTPMatchRequestBuilder()
+ .withNewUri()
+ .withNewStringMatchRegexType()
+ .withRegex(RECEIVE_PACK_URL_PATTERN)
+ .endStringMatchRegexType()
+ .endUri()
+ .build());
+ matches.add(
+ new HTTPMatchRequestBuilder()
+ .withNewUri()
+ .withNewStringMatchRegexType()
+ .withRegex(INFO_REF_URL_PATTERN)
+ .endStringMatchRegexType()
+ .endUri()
+ .withQueryParams(
+ Map.of(
+ "service",
+ new StringMatchBuilder()
+ .withNewStringMatchExactType("git-receive-pack")
+ .build()))
+ .build());
+ return matches;
+ }
+
+ private List<TCPRoute> getTCPRoutes(GerritNetwork gerritNetwork) {
+ List<TCPRoute> routes = new ArrayList<>();
+ for (NetworkMemberWithSsh gerrit : gerritNetwork.getSpec().getGerrits()) {
+ if (gerritNetwork.getSpec().getIngress().getSsh().isEnabled() && gerrit.getSshPort() > 0) {
+ routes.add(
+ new TCPRouteBuilder()
+ .withMatch(
+ List.of(new L4MatchAttributesBuilder().withPort(gerrit.getSshPort()).build()))
+ .withRoute(
+ getGerritTCPDestination(gerrit, gerritNetwork.getMetadata().getNamespace()))
+ .build());
+ }
+ }
+ return routes;
+ }
+
+ private RouteDestination getGerritTCPDestination(
+ NetworkMemberWithSsh networkMember, String namespace) {
+ return new RouteDestinationBuilder()
+ .withNewDestination()
+ .withHost(GerritService.getHostname(networkMember.getName(), namespace))
+ .withNewPort()
+ .withNumber(networkMember.getSshPort())
+ .endPort()
+ .endDestination()
+ .build();
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/none/GerritNoIngressReconciler.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/none/GerritNoIngressReconciler.java
new file mode 100644
index 0000000..2179260
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/network/none/GerritNoIngressReconciler.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.none;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import com.google.inject.Singleton;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
+import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
+
+@Singleton
+@ControllerConfiguration
+public class GerritNoIngressReconciler implements Reconciler<GerritNetwork> {
+
+ @Override
+ public UpdateControl<GerritNetwork> reconcile(
+ GerritNetwork resource, Context<GerritNetwork> context) throws Exception {
+ return UpdateControl.noUpdate();
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/ReceiverReconciler.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/ReceiverReconciler.java
new file mode 100644
index 0000000..b48a05f
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/ReceiverReconciler.java
@@ -0,0 +1,149 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.receiver;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.k8s.operator.receiver.dependent.ReceiverDeployment;
+import com.google.gerrit.k8s.operator.receiver.dependent.ReceiverService;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.Receiver;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.ReceiverStatus;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import io.fabric8.kubernetes.api.model.Secret;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
+import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
+import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer;
+import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
+import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.EventSource;
+import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper;
+import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Singleton
+@ControllerConfiguration(
+ dependents = {
+ @Dependent(name = "receiver-deployment", type = ReceiverDeployment.class),
+ @Dependent(
+ name = "receiver-service",
+ type = ReceiverService.class,
+ dependsOn = {"receiver-deployment"})
+ })
+public class ReceiverReconciler implements Reconciler<Receiver>, EventSourceInitializer<Receiver> {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private static final String SECRET_EVENT_SOURCE_NAME = "secret-event-source";
+ private final KubernetesClient client;
+
+ @Inject
+ public ReceiverReconciler(KubernetesClient client) {
+ this.client = client;
+ }
+
+ @Override
+ public Map<String, EventSource> prepareEventSources(EventSourceContext<Receiver> context) {
+ final SecondaryToPrimaryMapper<Secret> secretMapper =
+ (Secret secret) ->
+ context
+ .getPrimaryCache()
+ .list(
+ receiver ->
+ receiver
+ .getSpec()
+ .getCredentialSecretRef()
+ .equals(secret.getMetadata().getName()))
+ .map(ResourceID::fromResource)
+ .collect(Collectors.toSet());
+
+ InformerEventSource<Secret, Receiver> secretEventSource =
+ new InformerEventSource<>(
+ InformerConfiguration.from(Secret.class, context)
+ .withSecondaryToPrimaryMapper(secretMapper)
+ .build(),
+ context);
+
+ Map<String, EventSource> eventSources = new HashMap<>();
+ eventSources.put(SECRET_EVENT_SOURCE_NAME, secretEventSource);
+ return eventSources;
+ }
+
+ @Override
+ public UpdateControl<Receiver> reconcile(Receiver receiver, Context<Receiver> context)
+ throws Exception {
+ if (receiver.getStatus() != null && isReceiverRestartRequired(receiver, context)) {
+ restartReceiverDeployment(receiver);
+ }
+
+ return UpdateControl.patchStatus(updateStatus(receiver, context));
+ }
+
+ void restartReceiverDeployment(Receiver receiver) {
+ logger.atInfo().log(
+ "Restarting Receiver %s due to configuration change.", receiver.getMetadata().getName());
+ client
+ .apps()
+ .deployments()
+ .inNamespace(receiver.getMetadata().getNamespace())
+ .withName(receiver.getMetadata().getName())
+ .rolling()
+ .restart();
+ }
+
+ private Receiver updateStatus(Receiver receiver, Context<Receiver> context) {
+ ReceiverStatus status = receiver.getStatus();
+ if (status == null) {
+ status = new ReceiverStatus();
+ }
+
+ Secret sec =
+ client
+ .secrets()
+ .inNamespace(receiver.getMetadata().getNamespace())
+ .withName(receiver.getSpec().getCredentialSecretRef())
+ .get();
+
+ if (sec != null) {
+ status.setAppliedCredentialSecretVersion(sec.getMetadata().getResourceVersion());
+ }
+
+ receiver.setStatus(status);
+ return receiver;
+ }
+
+ private boolean isReceiverRestartRequired(Receiver receiver, Context<Receiver> context) {
+ String secVersion =
+ client
+ .secrets()
+ .inNamespace(receiver.getMetadata().getNamespace())
+ .withName(receiver.getSpec().getCredentialSecretRef())
+ .get()
+ .getMetadata()
+ .getResourceVersion();
+ String appliedSecVersion = receiver.getStatus().getAppliedCredentialSecretVersion();
+ if (!secVersion.equals(appliedSecVersion)) {
+ logger.atFine().log(
+ "Looking up Secret: %s; Installed secret resource version: %s; Resource version known to the Receiver: %s",
+ receiver.getSpec().getCredentialSecretRef(), secVersion, appliedSecVersion);
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/dependent/ReceiverDeployment.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/dependent/ReceiverDeployment.java
new file mode 100644
index 0000000..16c0072
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/dependent/ReceiverDeployment.java
@@ -0,0 +1,188 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.receiver.dependent;
+
+import com.google.gerrit.k8s.operator.receiver.ReceiverReconciler;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.Receiver;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.NfsWorkaroundConfig;
+import io.fabric8.kubernetes.api.model.Container;
+import io.fabric8.kubernetes.api.model.ContainerPort;
+import io.fabric8.kubernetes.api.model.Volume;
+import io.fabric8.kubernetes.api.model.VolumeBuilder;
+import io.fabric8.kubernetes.api.model.VolumeMount;
+import io.fabric8.kubernetes.api.model.VolumeMountBuilder;
+import io.fabric8.kubernetes.api.model.apps.Deployment;
+import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+@KubernetesDependent
+public class ReceiverDeployment extends CRUDKubernetesDependentResource<Deployment, Receiver> {
+ public static final int HTTP_PORT = 80;
+
+ public ReceiverDeployment() {
+ super(Deployment.class);
+ }
+
+ @Override
+ protected Deployment desired(Receiver receiver, Context<Receiver> context) {
+ DeploymentBuilder deploymentBuilder = new DeploymentBuilder();
+
+ List<Container> initContainers = new ArrayList<>();
+
+ NfsWorkaroundConfig nfsWorkaround =
+ receiver.getSpec().getStorage().getStorageClasses().getNfsWorkaround();
+ if (nfsWorkaround.isEnabled() && nfsWorkaround.isChownOnStartup()) {
+ initContainers.add(
+ GerritCluster.createNfsInitContainer(
+ receiver
+ .getSpec()
+ .getStorage()
+ .getStorageClasses()
+ .getNfsWorkaround()
+ .getIdmapdConfig()
+ != null,
+ receiver.getSpec().getContainerImages()));
+ }
+
+ deploymentBuilder
+ .withApiVersion("apps/v1")
+ .withNewMetadata()
+ .withName(receiver.getMetadata().getName())
+ .withNamespace(receiver.getMetadata().getNamespace())
+ .withLabels(getLabels(receiver))
+ .endMetadata()
+ .withNewSpec()
+ .withReplicas(receiver.getSpec().getReplicas())
+ .withNewStrategy()
+ .withNewRollingUpdate()
+ .withMaxSurge(receiver.getSpec().getMaxSurge())
+ .withMaxUnavailable(receiver.getSpec().getMaxUnavailable())
+ .endRollingUpdate()
+ .endStrategy()
+ .withNewSelector()
+ .withMatchLabels(getSelectorLabels(receiver))
+ .endSelector()
+ .withNewTemplate()
+ .withNewMetadata()
+ .withLabels(getLabels(receiver))
+ .endMetadata()
+ .withNewSpec()
+ .withTolerations(receiver.getSpec().getTolerations())
+ .withTopologySpreadConstraints(receiver.getSpec().getTopologySpreadConstraints())
+ .withAffinity(receiver.getSpec().getAffinity())
+ .withPriorityClassName(receiver.getSpec().getPriorityClassName())
+ .addAllToImagePullSecrets(receiver.getSpec().getContainerImages().getImagePullSecrets())
+ .withNewSecurityContext()
+ .withFsGroup(100L)
+ .endSecurityContext()
+ .addAllToInitContainers(initContainers)
+ .addNewContainer()
+ .withName("apache-git-http-backend")
+ .withImagePullPolicy(receiver.getSpec().getContainerImages().getImagePullPolicy())
+ .withImage(
+ receiver
+ .getSpec()
+ .getContainerImages()
+ .getGerritImages()
+ .getFullImageName("apache-git-http-backend"))
+ .withEnv(GerritCluster.getPodNameEnvVar())
+ .withPorts(getContainerPorts(receiver))
+ .withResources(receiver.getSpec().getResources())
+ .withReadinessProbe(receiver.getSpec().getReadinessProbe())
+ .withLivenessProbe(receiver.getSpec().getLivenessProbe())
+ .addAllToVolumeMounts(getVolumeMounts(receiver, false))
+ .endContainer()
+ .addAllToVolumes(getVolumes(receiver))
+ .endSpec()
+ .endTemplate()
+ .endSpec();
+
+ return deploymentBuilder.build();
+ }
+
+ private static String getComponentName(Receiver receiver) {
+ return String.format("receiver-deployment-%s", receiver.getMetadata().getName());
+ }
+
+ public static Map<String, String> getSelectorLabels(Receiver receiver) {
+ return GerritCluster.getSelectorLabels(
+ receiver.getMetadata().getName(), getComponentName(receiver));
+ }
+
+ public static Map<String, String> getLabels(Receiver receiver) {
+ return GerritCluster.getLabels(
+ receiver.getMetadata().getName(),
+ getComponentName(receiver),
+ ReceiverReconciler.class.getSimpleName());
+ }
+
+ private Set<Volume> getVolumes(Receiver receiver) {
+ Set<Volume> volumes = new HashSet<>();
+ volumes.add(
+ GerritCluster.getSharedVolume(
+ receiver.getSpec().getStorage().getSharedStorage().getExternalPVC()));
+
+ volumes.add(
+ new VolumeBuilder()
+ .withName(receiver.getSpec().getCredentialSecretRef())
+ .withNewSecret()
+ .withSecretName(receiver.getSpec().getCredentialSecretRef())
+ .endSecret()
+ .build());
+
+ NfsWorkaroundConfig nfsWorkaround =
+ receiver.getSpec().getStorage().getStorageClasses().getNfsWorkaround();
+ if (nfsWorkaround.isEnabled() && nfsWorkaround.getIdmapdConfig() != null) {
+ volumes.add(GerritCluster.getNfsImapdConfigVolume());
+ }
+
+ return volumes;
+ }
+
+ private Set<VolumeMount> getVolumeMounts(Receiver receiver, boolean isInitContainer) {
+ Set<VolumeMount> volumeMounts = new HashSet<>();
+ volumeMounts.add(GerritCluster.getGitRepositoriesVolumeMount("/var/gerrit/git"));
+ volumeMounts.add(GerritCluster.getLogsVolumeMount("/var/log/apache2"));
+
+ volumeMounts.add(
+ new VolumeMountBuilder()
+ .withName(receiver.getSpec().getCredentialSecretRef())
+ .withMountPath("/var/apache/credentials/.htpasswd")
+ .withSubPath(".htpasswd")
+ .build());
+
+ NfsWorkaroundConfig nfsWorkaround =
+ receiver.getSpec().getStorage().getStorageClasses().getNfsWorkaround();
+ if (nfsWorkaround.isEnabled() && nfsWorkaround.getIdmapdConfig() != null) {
+ volumeMounts.add(GerritCluster.getNfsImapdConfigVolumeMount());
+ }
+
+ return volumeMounts;
+ }
+
+ private List<ContainerPort> getContainerPorts(Receiver receiver) {
+ List<ContainerPort> containerPorts = new ArrayList<>();
+ containerPorts.add(new ContainerPort(HTTP_PORT, null, null, "http", null));
+ return containerPorts;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/dependent/ReceiverSecret.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/dependent/ReceiverSecret.java
new file mode 100644
index 0000000..ca4700a
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/dependent/ReceiverSecret.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.receiver.dependent;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.Receiver;
+import io.fabric8.kubernetes.api.model.Secret;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource;
+import io.javaoperatorsdk.operator.processing.event.ResourceID;
+import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@KubernetesDependent
+public class ReceiverSecret extends KubernetesDependentResource<Secret, Receiver>
+ implements SecondaryToPrimaryMapper<Secret> {
+ public ReceiverSecret() {
+ super(Secret.class);
+ }
+
+ @Override
+ public Set<ResourceID> toPrimaryResourceIDs(Secret secret) {
+ return client
+ .resources(Receiver.class)
+ .inNamespace(secret.getMetadata().getNamespace())
+ .list()
+ .getItems()
+ .stream()
+ .filter(g -> g.getSpec().getCredentialSecretRef().equals(secret.getMetadata().getName()))
+ .map(g -> ResourceID.fromResource(g))
+ .collect(Collectors.toSet());
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/dependent/ReceiverService.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/dependent/ReceiverService.java
new file mode 100644
index 0000000..3ccb644
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/dependent/ReceiverService.java
@@ -0,0 +1,91 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.receiver.dependent;
+
+import static com.google.gerrit.k8s.operator.receiver.dependent.ReceiverDeployment.HTTP_PORT;
+
+import com.google.gerrit.k8s.operator.receiver.ReceiverReconciler;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.Receiver;
+import io.fabric8.kubernetes.api.model.Service;
+import io.fabric8.kubernetes.api.model.ServiceBuilder;
+import io.fabric8.kubernetes.api.model.ServicePort;
+import io.fabric8.kubernetes.api.model.ServicePortBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+@KubernetesDependent
+public class ReceiverService extends CRUDKubernetesDependentResource<Service, Receiver> {
+ public static final String HTTP_PORT_NAME = "http";
+
+ public ReceiverService() {
+ super(Service.class);
+ }
+
+ @Override
+ protected Service desired(Receiver receiver, Context<Receiver> context) {
+ return new ServiceBuilder()
+ .withApiVersion("v1")
+ .withNewMetadata()
+ .withName(getName(receiver))
+ .withNamespace(receiver.getMetadata().getNamespace())
+ .withLabels(getLabels(receiver))
+ .endMetadata()
+ .withNewSpec()
+ .withType(receiver.getSpec().getService().getType())
+ .withPorts(getServicePorts(receiver))
+ .withSelector(ReceiverDeployment.getSelectorLabels(receiver))
+ .endSpec()
+ .build();
+ }
+
+ public static String getName(Receiver receiver) {
+ return receiver.getMetadata().getName();
+ }
+
+ public static String getName(String receiverName) {
+ return receiverName;
+ }
+
+ public static Map<String, String> getLabels(Receiver receiver) {
+ return GerritCluster.getLabels(
+ receiver.getMetadata().getName(),
+ "receiver-service",
+ ReceiverReconciler.class.getSimpleName());
+ }
+
+ public static String getHostname(Receiver receiver) {
+ return getHostname(receiver.getMetadata().getName(), receiver.getMetadata().getNamespace());
+ }
+
+ public static String getHostname(String receiverName, String namespace) {
+ return String.format("%s.%s.svc.cluster.local", getName(receiverName), namespace);
+ }
+
+ private static List<ServicePort> getServicePorts(Receiver receiver) {
+ List<ServicePort> ports = new ArrayList<>();
+ ports.add(
+ new ServicePortBuilder()
+ .withName(HTTP_PORT_NAME)
+ .withPort(receiver.getSpec().getService().getHttpPort())
+ .withNewTargetPort(HTTP_PORT)
+ .build());
+ return ports;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/AbstractKeyStoreProvider.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/AbstractKeyStoreProvider.java
new file mode 100644
index 0000000..3b71e89
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/AbstractKeyStoreProvider.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.server;
+
+import java.io.IOException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.util.Base64;
+
+public abstract class AbstractKeyStoreProvider implements KeyStoreProvider {
+ private static final String ALIAS = "operator";
+ private static final String CERT_PREFIX = "-----BEGIN CERTIFICATE-----";
+ private static final String CERT_SUFFIX = "-----END CERTIFICATE-----";
+
+ final String getAlias() {
+ return ALIAS;
+ }
+
+ @Override
+ public final String getCertificate()
+ throws CertificateEncodingException, KeyStoreException, NoSuchAlgorithmException,
+ CertificateException, IOException {
+ StringBuilder cert = new StringBuilder();
+ cert.append(CERT_PREFIX);
+ cert.append("\n");
+ cert.append(
+ Base64.getEncoder().encodeToString(getKeyStore().getCertificate(getAlias()).getEncoded()));
+ cert.append("\n");
+ cert.append(CERT_SUFFIX);
+ return cert.toString();
+ }
+
+ private final KeyStore getKeyStore()
+ throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException {
+ return KeyStore.getInstance(getKeyStorePath().toFile(), getKeyStorePassword().toCharArray());
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/AdmissionWebhookServlet.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/AdmissionWebhookServlet.java
new file mode 100644
index 0000000..a886988
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/AdmissionWebhookServlet.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.server;
+
+import jakarta.servlet.http.HttpServlet;
+
+public abstract class AdmissionWebhookServlet extends HttpServlet {
+ private static final long serialVersionUID = 1L;
+
+ public abstract String getName();
+
+ public abstract String getVersion();
+
+ public abstract String getURI();
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/FileSystemKeyStoreProvider.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/FileSystemKeyStoreProvider.java
new file mode 100644
index 0000000..a56da7f
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/FileSystemKeyStoreProvider.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.server;
+
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+@Singleton
+public class FileSystemKeyStoreProvider extends AbstractKeyStoreProvider {
+ static final String KEYSTORE_PATH = "/operator/keystore.jks";
+ static final String KEYSTORE_PWD_FILE = "/operator/keystore.password";
+
+ @Override
+ public Path getKeyStorePath() {
+ return Path.of(KEYSTORE_PATH);
+ }
+
+ @Override
+ public String getKeyStorePassword() throws IOException {
+ return Files.readString(Path.of(KEYSTORE_PWD_FILE));
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/GeneratedKeyStoreProvider.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/GeneratedKeyStoreProvider.java
new file mode 100644
index 0000000..d96204d
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/GeneratedKeyStoreProvider.java
@@ -0,0 +1,132 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.server;
+
+import static com.google.gerrit.k8s.operator.GerritOperator.SERVICE_NAME;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.nio.file.Path;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.Security;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Date;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.bouncycastle.asn1.ASN1Encodable;
+import org.bouncycastle.asn1.DERSequence;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.GeneralName;
+import org.bouncycastle.cert.CertIOException;
+import org.bouncycastle.cert.X509v3CertificateBuilder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+
+@Singleton
+public class GeneratedKeyStoreProvider extends AbstractKeyStoreProvider {
+ private static final Path KEYSTORE_PATH = Path.of("/tmp/keystore.jks");
+
+ private final String namespace;
+ private final String password;
+
+ @Inject
+ public GeneratedKeyStoreProvider(@Named("Namespace") String namespace) {
+ this.namespace = namespace;
+ this.password = generatePassword();
+ generateKeyStore();
+ }
+
+ @Override
+ public Path getKeyStorePath() {
+ return KEYSTORE_PATH;
+ }
+
+ @Override
+ public String getKeyStorePassword() {
+ return password;
+ }
+
+ private String getCN() {
+ return String.format("%s.%s.svc", SERVICE_NAME, namespace);
+ }
+
+ private String generatePassword() {
+ return RandomStringUtils.randomAlphabetic(10);
+ }
+
+ private Certificate generateCertificate(KeyPair keyPair)
+ throws OperatorCreationException, CertificateException, CertIOException {
+ BouncyCastleProvider bcProvider = new BouncyCastleProvider();
+ Security.addProvider(bcProvider);
+
+ Instant start = Instant.now();
+ X500Name dnName = new X500Name(String.format("cn=%s", getCN()));
+ DERSequence subjectAlternativeNames =
+ new DERSequence(new ASN1Encodable[] {new GeneralName(GeneralName.dNSName, getCN())});
+
+ X509v3CertificateBuilder certBuilder =
+ new JcaX509v3CertificateBuilder(
+ dnName,
+ BigInteger.valueOf(start.toEpochMilli()),
+ Date.from(start),
+ Date.from(start.plus(365, ChronoUnit.DAYS)),
+ dnName,
+ keyPair.getPublic())
+ .addExtension(Extension.subjectAlternativeName, true, subjectAlternativeNames);
+
+ ContentSigner contentSigner =
+ new JcaContentSignerBuilder("SHA256WithRSA").build(keyPair.getPrivate());
+ return new JcaX509CertificateConverter()
+ .setProvider(bcProvider)
+ .getCertificate(certBuilder.build(contentSigner));
+ }
+
+ private void generateKeyStore() {
+ KEYSTORE_PATH.getParent().toFile().mkdirs();
+ try (FileOutputStream fos = new FileOutputStream(KEYSTORE_PATH.toFile())) {
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
+ keyPairGenerator.initialize(4096);
+ KeyPair keyPair = keyPairGenerator.generateKeyPair();
+
+ Certificate[] chain = {generateCertificate(keyPair)};
+
+ KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+ keyStore.load(null, null);
+ keyStore.setKeyEntry(getAlias(), keyPair.getPrivate(), password.toCharArray(), chain);
+ keyStore.store(fos, password.toCharArray());
+ } catch (IOException
+ | NoSuchAlgorithmException
+ | CertificateException
+ | KeyStoreException
+ | OperatorCreationException e) {
+ throw new IllegalStateException("Failed to create keystore.", e);
+ }
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/HealthcheckServlet.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/HealthcheckServlet.java
new file mode 100644
index 0000000..6097bde
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/HealthcheckServlet.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.server;
+
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+public class HealthcheckServlet extends HttpServlet {
+ private static final long serialVersionUID = 1L;
+
+ protected void doGet(HttpServletRequest request, HttpServletResponse response)
+ throws IOException {
+ response.setContentType("application/text");
+ response.setStatus(HttpServletResponse.SC_OK);
+ response.getWriter().println("ALL GOOD.");
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/HttpServer.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/HttpServer.java
new file mode 100644
index 0000000..23b8055
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/HttpServer.java
@@ -0,0 +1,72 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.server;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.Set;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.SecureRequestCustomizer;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.servlet.ServletHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+
+@Singleton
+public class HttpServer {
+ public static final String KEYSTORE_PATH = "/operator/keystore.jks";
+ public static final String KEYSTORE_PWD_FILE = "/operator/keystore.password";
+ public static final int PORT = 8080;
+
+ private final Server server = new Server();
+ private final KeyStoreProvider keyStoreProvider;
+ private final Set<AdmissionWebhookServlet> admissionWebhookServlets;
+
+ @Inject
+ public HttpServer(
+ KeyStoreProvider keyStoreProvider, Set<AdmissionWebhookServlet> admissionWebhookServlets) {
+ this.keyStoreProvider = keyStoreProvider;
+ this.admissionWebhookServlets = admissionWebhookServlets;
+ }
+
+ public void start() throws Exception {
+ SslContextFactory.Server ssl = new SslContextFactory.Server();
+ ssl.setKeyStorePath(keyStoreProvider.getKeyStorePath().toString());
+ ssl.setTrustStorePath(keyStoreProvider.getKeyStorePath().toString());
+ ssl.setKeyStorePassword(keyStoreProvider.getKeyStorePassword());
+ ssl.setTrustStorePassword(keyStoreProvider.getKeyStorePassword());
+ ssl.setSniRequired(false);
+
+ HttpConfiguration sslConfiguration = new HttpConfiguration();
+ sslConfiguration.addCustomizer(new SecureRequestCustomizer(false));
+ HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory(sslConfiguration);
+
+ ServerConnector connector = new ServerConnector(server, ssl, httpConnectionFactory);
+ connector.setPort(PORT);
+ server.setConnectors(new Connector[] {connector});
+
+ ServletHandler servletHandler = new ServletHandler();
+ for (AdmissionWebhookServlet servlet : admissionWebhookServlets) {
+ servletHandler.addServletWithMapping(new ServletHolder(servlet), servlet.getURI());
+ }
+ servletHandler.addServletWithMapping(HealthcheckServlet.class, "/health");
+ server.setHandler(servletHandler);
+
+ server.start();
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/KeyStoreProvider.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/KeyStoreProvider.java
new file mode 100644
index 0000000..c41777f
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/KeyStoreProvider.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.server;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+
+public interface KeyStoreProvider {
+ Path getKeyStorePath();
+
+ String getKeyStorePassword() throws IOException;
+
+ String getCertificate()
+ throws CertificateEncodingException, KeyStoreException, NoSuchAlgorithmException,
+ CertificateException, IOException;
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/ServerModule.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/ServerModule.java
new file mode 100644
index 0000000..ae613dd
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/ServerModule.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.server;
+
+import static com.google.gerrit.k8s.operator.server.FileSystemKeyStoreProvider.KEYSTORE_PATH;
+
+import com.google.gerrit.k8s.operator.v1alpha.admission.servlet.GerritAdmissionWebhook;
+import com.google.gerrit.k8s.operator.v1alpha.admission.servlet.GerritClusterAdmissionWebhook;
+import com.google.gerrit.k8s.operator.v1alpha.admission.servlet.GitGcAdmissionWebhook;
+import com.google.inject.AbstractModule;
+import com.google.inject.multibindings.Multibinder;
+import java.io.File;
+
+public class ServerModule extends AbstractModule {
+ public void configure() {
+ if (new File(KEYSTORE_PATH).exists()) {
+ bind(KeyStoreProvider.class).to(FileSystemKeyStoreProvider.class);
+ } else {
+ bind(KeyStoreProvider.class).to(GeneratedKeyStoreProvider.class);
+ }
+ bind(HttpServer.class);
+ Multibinder<AdmissionWebhookServlet> admissionWebhookServlets =
+ Multibinder.newSetBinder(binder(), AdmissionWebhookServlet.class);
+ admissionWebhookServlets.addBinding().to(GerritClusterAdmissionWebhook.class);
+ admissionWebhookServlets.addBinding().to(GitGcAdmissionWebhook.class);
+ admissionWebhookServlets.addBinding().to(GerritAdmissionWebhook.class);
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/ValidatingAdmissionWebhookServlet.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/ValidatingAdmissionWebhookServlet.java
new file mode 100644
index 0000000..f6d7c39
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/server/ValidatingAdmissionWebhookServlet.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.server;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.flogger.FluentLogger;
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.fabric8.kubernetes.api.model.Status;
+import io.fabric8.kubernetes.api.model.admission.v1.AdmissionResponseBuilder;
+import io.fabric8.kubernetes.api.model.admission.v1.AdmissionReview;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+public abstract class ValidatingAdmissionWebhookServlet extends AdmissionWebhookServlet {
+ private static final long serialVersionUID = 1L;
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ public abstract Status validate(HasMetadata resource);
+
+ @Override
+ public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
+ ObjectMapper objectMapper = new ObjectMapper();
+ AdmissionReview admissionReq =
+ objectMapper.readValue(request.getInputStream(), AdmissionReview.class);
+
+ logger.atFine().log("Admission request received: %s", admissionReq.toString());
+
+ response.setContentType("application/json");
+ AdmissionResponseBuilder admissionRespBuilder =
+ new AdmissionResponseBuilder().withUid(admissionReq.getRequest().getUid());
+ Status validationStatus = validate((HasMetadata) admissionReq.getRequest().getObject());
+ response.setStatus(HttpServletResponse.SC_OK);
+ if (validationStatus.getCode() < 400) {
+ admissionRespBuilder = admissionRespBuilder.withAllowed(true);
+ } else {
+ admissionRespBuilder = admissionRespBuilder.withAllowed(false).withStatus(validationStatus);
+ }
+ admissionReq.setResponse(admissionRespBuilder.build());
+ objectMapper.writeValue(response.getWriter(), admissionReq);
+ logger.atFine().log(
+ "Admission request responded with %s", admissionReq.getResponse().toString());
+ }
+
+ @Override
+ public String getURI() {
+ return String.format("/admission/%s/%s", getVersion(), getName());
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/util/CRUDKubernetesDependentPVCResource.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/util/CRUDKubernetesDependentPVCResource.java
new file mode 100644
index 0000000..cd5bd8e
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/util/CRUDKubernetesDependentPVCResource.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.util;
+
+import com.google.common.flogger.FluentLogger;
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.fabric8.kubernetes.api.model.PersistentVolumeClaim;
+import io.fabric8.kubernetes.api.model.PersistentVolumeClaimSpec;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
+
+public abstract class CRUDKubernetesDependentPVCResource<P extends HasMetadata>
+ extends CRUDKubernetesDependentResource<PersistentVolumeClaim, P> {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ public CRUDKubernetesDependentPVCResource() {
+ super(PersistentVolumeClaim.class);
+ }
+
+ @Override
+ protected final PersistentVolumeClaim desired(P primary, Context<P> context) {
+ PersistentVolumeClaim pvc = desiredPVC(primary, context);
+ PersistentVolumeClaim existingPvc =
+ client
+ .persistentVolumeClaims()
+ .inNamespace(pvc.getMetadata().getNamespace())
+ .withName(pvc.getMetadata().getName())
+ .get();
+ String volumeName = pvc.getSpec().getVolumeName();
+ if (existingPvc != null && (volumeName == null || volumeName.isEmpty())) {
+ logger.atFine().log(
+ "PVC %s/%s already has bound a PV. Keeping volumeName reference.",
+ pvc.getMetadata().getNamespace(), pvc.getMetadata().getName());
+ PersistentVolumeClaimSpec pvcSpec = pvc.getSpec();
+ pvcSpec.setVolumeName(existingPvc.getSpec().getVolumeName());
+ pvc.setSpec(pvcSpec);
+ }
+ return pvc;
+ }
+
+ protected abstract PersistentVolumeClaim desiredPVC(P primary, Context<P> context);
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/admission/servlet/GerritAdmissionWebhook.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/admission/servlet/GerritAdmissionWebhook.java
new file mode 100644
index 0000000..f74cb59
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/admission/servlet/GerritAdmissionWebhook.java
@@ -0,0 +1,116 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.admission.servlet;
+
+import static com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GlobalRefDbConfig.RefDatabase.SPANNER;
+import static com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GlobalRefDbConfig.RefDatabase.ZOOKEEPER;
+
+import com.google.gerrit.k8s.operator.gerrit.config.InvalidGerritConfigException;
+import com.google.gerrit.k8s.operator.server.ValidatingAdmissionWebhookServlet;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GlobalRefDbConfig;
+import com.google.gerrit.k8s.operator.v1alpha.gerrit.config.GerritConfigBuilder;
+import com.google.inject.Singleton;
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.fabric8.kubernetes.api.model.Status;
+import io.fabric8.kubernetes.api.model.StatusBuilder;
+import jakarta.servlet.http.HttpServletResponse;
+import java.util.Locale;
+
+@Singleton
+public class GerritAdmissionWebhook extends ValidatingAdmissionWebhookServlet {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public Status validate(HasMetadata resource) {
+ if (!(resource instanceof Gerrit)) {
+ return new StatusBuilder()
+ .withCode(HttpServletResponse.SC_BAD_REQUEST)
+ .withMessage("Invalid resource. Expected Gerrit-resource for validation.")
+ .build();
+ }
+
+ Gerrit gerrit = (Gerrit) resource;
+
+ try {
+ invalidGerritConfiguration(gerrit);
+ } catch (InvalidGerritConfigException e) {
+ return new StatusBuilder()
+ .withCode(HttpServletResponse.SC_BAD_REQUEST)
+ .withMessage(e.getMessage())
+ .build();
+ }
+
+ if (noRefDbConfiguredForHA(gerrit)) {
+ return new StatusBuilder()
+ .withCode(HttpServletResponse.SC_BAD_REQUEST)
+ .withMessage(
+ "A Ref-Database is required to horizontally scale a primary Gerrit: .spec.refdb.database != NONE")
+ .build();
+ }
+
+ if (missingRefdbConfig(gerrit)) {
+ String refDbName = "";
+ switch (gerrit.getSpec().getRefdb().getDatabase()) {
+ case ZOOKEEPER:
+ refDbName = ZOOKEEPER.toString().toLowerCase(Locale.US);
+ break;
+ case SPANNER:
+ refDbName = SPANNER.toString().toLowerCase(Locale.US);
+ break;
+ default:
+ break;
+ }
+ return new StatusBuilder()
+ .withCode(HttpServletResponse.SC_BAD_REQUEST)
+ .withMessage(
+ String.format("Missing %s configuration (.spec.refdb.%s)", refDbName, refDbName))
+ .build();
+ }
+
+ return new StatusBuilder().withCode(HttpServletResponse.SC_OK).build();
+ }
+
+ private void invalidGerritConfiguration(Gerrit gerrit) throws InvalidGerritConfigException {
+ new GerritConfigBuilder(gerrit).validate();
+ }
+
+ private boolean noRefDbConfiguredForHA(Gerrit gerrit) {
+ return gerrit.getSpec().isHighlyAvailablePrimary()
+ && gerrit.getSpec().getRefdb().getDatabase().equals(GlobalRefDbConfig.RefDatabase.NONE);
+ }
+
+ private boolean missingRefdbConfig(Gerrit gerrit) {
+ GlobalRefDbConfig refDbConfig = gerrit.getSpec().getRefdb();
+ switch (refDbConfig.getDatabase()) {
+ case ZOOKEEPER:
+ return refDbConfig.getZookeeper() == null;
+ case SPANNER:
+ return refDbConfig.getSpanner() == null;
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public String getName() {
+ return "gerrit";
+ }
+
+ @Override
+ public String getVersion() {
+ return "v1alpha";
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/admission/servlet/GerritClusterAdmissionWebhook.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/admission/servlet/GerritClusterAdmissionWebhook.java
new file mode 100644
index 0000000..8817559
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/admission/servlet/GerritClusterAdmissionWebhook.java
@@ -0,0 +1,103 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.admission.servlet;
+
+import com.google.gerrit.k8s.operator.server.ValidatingAdmissionWebhookServlet;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplate;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplateSpec.GerritMode;
+import com.google.inject.Singleton;
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.fabric8.kubernetes.api.model.Status;
+import io.fabric8.kubernetes.api.model.StatusBuilder;
+import jakarta.servlet.http.HttpServletResponse;
+
+@Singleton
+public class GerritClusterAdmissionWebhook extends ValidatingAdmissionWebhookServlet {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public Status validate(HasMetadata resource) {
+ if (!(resource instanceof GerritCluster)) {
+ return new StatusBuilder()
+ .withCode(HttpServletResponse.SC_BAD_REQUEST)
+ .withMessage("Invalid resource. Expected GerritCluster-resource for validation.")
+ .build();
+ }
+
+ GerritCluster gerritCluster = (GerritCluster) resource;
+
+ if (multiplePrimaryGerritInCluster(gerritCluster)) {
+ return new StatusBuilder()
+ .withCode(HttpServletResponse.SC_CONFLICT)
+ .withMessage("Only a single primary Gerrit is allowed per Gerrit Cluster.")
+ .build();
+ }
+
+ if (primaryGerritAndReceiverInCluster(gerritCluster)) {
+ return new StatusBuilder()
+ .withCode(HttpServletResponse.SC_CONFLICT)
+ .withMessage("A primary Gerrit cannot be in the same Gerrit Cluster as a Receiver.")
+ .build();
+ }
+
+ if (multipleGerritReplicaInCluster(gerritCluster)) {
+ return new StatusBuilder()
+ .withCode(HttpServletResponse.SC_CONFLICT)
+ .withMessage("Only a single Gerrit Replica is allowed per Gerrit Cluster.")
+ .build();
+ }
+
+ GerritAdmissionWebhook gerritAdmission = new GerritAdmissionWebhook();
+ for (GerritTemplate gerrit : gerritCluster.getSpec().getGerrits()) {
+ Status status = gerritAdmission.validate(gerrit.toGerrit(gerritCluster));
+ if (status.getCode() != HttpServletResponse.SC_OK) {
+ return status;
+ }
+ }
+
+ return new StatusBuilder().withCode(HttpServletResponse.SC_OK).build();
+ }
+
+ private boolean multiplePrimaryGerritInCluster(GerritCluster gerritCluster) {
+ return gerritCluster.getSpec().getGerrits().stream()
+ .filter(g -> g.getSpec().getMode() == GerritMode.PRIMARY)
+ .count()
+ > 1;
+ }
+
+ private boolean primaryGerritAndReceiverInCluster(GerritCluster gerritCluster) {
+ return gerritCluster.getSpec().getGerrits().stream()
+ .anyMatch(g -> g.getSpec().getMode() == GerritMode.PRIMARY)
+ && gerritCluster.getSpec().getReceiver() != null;
+ }
+
+ private boolean multipleGerritReplicaInCluster(GerritCluster gerritCluster) {
+ return gerritCluster.getSpec().getGerrits().stream()
+ .filter(g -> g.getSpec().getMode() == GerritMode.REPLICA)
+ .count()
+ > 1;
+ }
+
+ @Override
+ public String getName() {
+ return "gerritcluster";
+ }
+
+ @Override
+ public String getVersion() {
+ return "v1alpha";
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/admission/servlet/GitGcAdmissionWebhook.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/admission/servlet/GitGcAdmissionWebhook.java
new file mode 100644
index 0000000..3198c1a
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/admission/servlet/GitGcAdmissionWebhook.java
@@ -0,0 +1,114 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.admission.servlet;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.k8s.operator.server.ValidatingAdmissionWebhookServlet;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gitgc.GitGarbageCollection;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.fabric8.kubernetes.api.model.Status;
+import io.fabric8.kubernetes.api.model.StatusBuilder;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import jakarta.servlet.http.HttpServletResponse;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Singleton
+public class GitGcAdmissionWebhook extends ValidatingAdmissionWebhookServlet {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private static final long serialVersionUID = 1L;
+ private static final Status OK_STATUS =
+ new StatusBuilder().withCode(HttpServletResponse.SC_OK).build();
+
+ private final KubernetesClient client;
+
+ @Inject
+ public GitGcAdmissionWebhook(KubernetesClient client) {
+ this.client = client;
+ }
+
+ @Override
+ public Status validate(HasMetadata resource) {
+ if (!(resource instanceof GitGarbageCollection)) {
+ return new StatusBuilder()
+ .withCode(HttpServletResponse.SC_BAD_REQUEST)
+ .withMessage("Invalid resource. Expected GitGarbageCollection-resource for validation.")
+ .build();
+ }
+
+ GitGarbageCollection gitGc = (GitGarbageCollection) resource;
+
+ String gitGcUid = gitGc.getMetadata().getUid();
+ List<GitGarbageCollection> gitGcs =
+ client
+ .resources(GitGarbageCollection.class)
+ .inNamespace(gitGc.getMetadata().getNamespace())
+ .list()
+ .getItems()
+ .stream()
+ .filter(gc -> !gc.getMetadata().getUid().equals(gitGcUid))
+ .collect(Collectors.toList());
+ Set<String> projects = gitGc.getSpec().getProjects();
+
+ logger.atFine().log("Detected GitGcs: %s", gitGcs);
+ if (projects.isEmpty()) {
+ if (gitGcs.stream().anyMatch(gc -> gc.getSpec().getProjects().isEmpty())) {
+ return new StatusBuilder()
+ .withCode(HttpServletResponse.SC_CONFLICT)
+ .withMessage("Only a single GitGc working on all projects allowed per GerritCluster.")
+ .build();
+ }
+ return OK_STATUS;
+ }
+
+ Set<String> projectsWithExistingGC =
+ gitGcs.stream()
+ .map(gc -> gc.getSpec().getProjects())
+ .flatMap(Collection::stream)
+ .collect(Collectors.toSet());
+ Set<String> projectsIntersection = getIntersection(projects, projectsWithExistingGC);
+ if (projectsIntersection.isEmpty()) {
+ return OK_STATUS;
+ }
+ return new StatusBuilder()
+ .withCode(HttpServletResponse.SC_CONFLICT)
+ .withMessage(
+ "Only a single GitGc is allowed to work on a given project. Conflict for projects: "
+ + projectsIntersection)
+ .build();
+ }
+
+ private Set<String> getIntersection(Set<String> set1, Set<String> set2) {
+ Set<String> intersection = new HashSet<>();
+ intersection.addAll(set1);
+ intersection.retainAll(set2);
+ return intersection;
+ }
+
+ @Override
+ public String getName() {
+ return "gitgc";
+ }
+
+ @Override
+ public String getVersion() {
+ return "v1alpha";
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/cluster/GerritCluster.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/cluster/GerritCluster.java
new file mode 100644
index 0000000..b88ab09
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/cluster/GerritCluster.java
@@ -0,0 +1,242 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.cluster;
+
+import static com.google.gerrit.k8s.operator.cluster.dependent.NfsIdmapdConfigMap.NFS_IDMAPD_CM_NAME;
+import static com.google.gerrit.k8s.operator.cluster.dependent.SharedPVC.SHARED_PVC_NAME;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.ContainerImageConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.SharedStorage.ExternalPVCConfig;
+import io.fabric8.kubernetes.api.model.Container;
+import io.fabric8.kubernetes.api.model.ContainerBuilder;
+import io.fabric8.kubernetes.api.model.EnvVar;
+import io.fabric8.kubernetes.api.model.EnvVarBuilder;
+import io.fabric8.kubernetes.api.model.Namespaced;
+import io.fabric8.kubernetes.api.model.Volume;
+import io.fabric8.kubernetes.api.model.VolumeBuilder;
+import io.fabric8.kubernetes.api.model.VolumeMount;
+import io.fabric8.kubernetes.api.model.VolumeMountBuilder;
+import io.fabric8.kubernetes.client.CustomResource;
+import io.fabric8.kubernetes.model.annotation.Group;
+import io.fabric8.kubernetes.model.annotation.ShortNames;
+import io.fabric8.kubernetes.model.annotation.Version;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+@Group("gerritoperator.google.com")
+@Version("v1alpha17")
+@ShortNames("gclus")
+public class GerritCluster extends CustomResource<GerritClusterSpec, GerritClusterStatus>
+ implements Namespaced {
+ private static final long serialVersionUID = 2L;
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private static final String SHARED_VOLUME_NAME = "shared";
+ private static final String NFS_IDMAPD_CONFIG_VOLUME_NAME = "nfs-config";
+ private static final int GERRIT_FS_UID = 1000;
+ private static final int GERRIT_FS_GID = 100;
+ public static final String PLUGIN_CACHE_MOUNT_PATH = "/var/mnt/plugin_cache";
+ public static final String PLUGIN_CACHE_SUB_DIR = "plugin_cache";
+
+ public String toString() {
+ return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE);
+ }
+
+ @JsonIgnore
+ public Map<String, String> getLabels(String component, String createdBy) {
+ return getLabels(getMetadata().getName(), component, createdBy);
+ }
+
+ // TODO(Thomas): Having so many string parameters is bad. The only parameter should be the
+ // Kubernetes resource that implements an interface that provides methods to retrieve the
+ // required information.
+ @JsonIgnore
+ public static Map<String, String> getLabels(String instance, String component, String createdBy) {
+ Map<String, String> labels = new HashMap<>();
+
+ labels.putAll(getSelectorLabels(instance, component));
+ String version = GerritCluster.class.getPackage().getImplementationVersion();
+ if (version == null || version.isBlank()) {
+ logger.atWarning().log("Unable to read Gerrit Operator version from jar.");
+ version = "unknown";
+ }
+ labels.put("app.kubernetes.io/version", version);
+ labels.put("app.kubernetes.io/created-by", createdBy);
+
+ return labels;
+ }
+
+ @JsonIgnore
+ public static Map<String, String> getSelectorLabels(String instance, String component) {
+ Map<String, String> labels = new HashMap<>();
+
+ labels.put("app.kubernetes.io/name", "gerrit");
+ labels.put("app.kubernetes.io/instance", instance);
+ labels.put("app.kubernetes.io/component", component);
+ labels.put("app.kubernetes.io/part-of", instance);
+ labels.put("app.kubernetes.io/managed-by", "gerrit-operator");
+
+ return labels;
+ }
+
+ @JsonIgnore
+ public static Volume getSharedVolume(ExternalPVCConfig externalPVC) {
+ String claimName = externalPVC.isEnabled() ? externalPVC.getClaimName() : SHARED_PVC_NAME;
+ return new VolumeBuilder()
+ .withName(SHARED_VOLUME_NAME)
+ .withNewPersistentVolumeClaim()
+ .withClaimName(claimName)
+ .endPersistentVolumeClaim()
+ .build();
+ }
+
+ @JsonIgnore
+ public static VolumeMount getGitRepositoriesVolumeMount() {
+ return getGitRepositoriesVolumeMount("/var/mnt/git");
+ }
+
+ @JsonIgnore
+ public static VolumeMount getGitRepositoriesVolumeMount(String mountPath) {
+ return new VolumeMountBuilder()
+ .withName(SHARED_VOLUME_NAME)
+ .withSubPath("git")
+ .withMountPath(mountPath)
+ .build();
+ }
+
+ @JsonIgnore
+ public static VolumeMount getHAShareVolumeMount() {
+ return getSharedVolumeMount("shared", "/var/mnt/shared");
+ }
+
+ @JsonIgnore
+ public static VolumeMount getPluginCacheVolumeMount() {
+ return getSharedVolumeMount(PLUGIN_CACHE_SUB_DIR, "/var/mnt/plugin_cache");
+ }
+
+ @JsonIgnore
+ public static VolumeMount getSharedVolumeMount(String subPath, String mountPath) {
+ return new VolumeMountBuilder()
+ .withName(SHARED_VOLUME_NAME)
+ .withSubPath(subPath)
+ .withMountPath(mountPath)
+ .build();
+ }
+
+ @JsonIgnore
+ public static VolumeMount getLogsVolumeMount() {
+ return getLogsVolumeMount("/var/mnt/logs");
+ }
+
+ @JsonIgnore
+ public static VolumeMount getLogsVolumeMount(String mountPath) {
+ return new VolumeMountBuilder()
+ .withName(SHARED_VOLUME_NAME)
+ .withSubPathExpr("logs/$(POD_NAME)")
+ .withMountPath(mountPath)
+ .build();
+ }
+
+ @JsonIgnore
+ public static Volume getNfsImapdConfigVolume() {
+ return new VolumeBuilder()
+ .withName(NFS_IDMAPD_CONFIG_VOLUME_NAME)
+ .withNewConfigMap()
+ .withName(NFS_IDMAPD_CM_NAME)
+ .endConfigMap()
+ .build();
+ }
+
+ @JsonIgnore
+ public static VolumeMount getNfsImapdConfigVolumeMount() {
+ return new VolumeMountBuilder()
+ .withName(NFS_IDMAPD_CONFIG_VOLUME_NAME)
+ .withMountPath("/etc/idmapd.conf")
+ .withSubPath("idmapd.conf")
+ .build();
+ }
+
+ @JsonIgnore
+ public Container createNfsInitContainer() {
+ return createNfsInitContainer(
+ getSpec().getStorage().getStorageClasses().getNfsWorkaround().getIdmapdConfig() != null,
+ getSpec().getContainerImages());
+ }
+
+ @JsonIgnore
+ public static Container createNfsInitContainer(
+ boolean configureIdmapd, ContainerImageConfig imageConfig) {
+ return createNfsInitContainer(configureIdmapd, imageConfig, List.of());
+ }
+
+ @JsonIgnore
+ public static Container createNfsInitContainer(
+ boolean configureIdmapd,
+ ContainerImageConfig imageConfig,
+ List<VolumeMount> additionalVolumeMounts) {
+ List<VolumeMount> volumeMounts = new ArrayList<>();
+ volumeMounts.add(getLogsVolumeMount());
+ volumeMounts.add(getGitRepositoriesVolumeMount());
+
+ volumeMounts.addAll(additionalVolumeMounts);
+
+ if (configureIdmapd) {
+ volumeMounts.add(getNfsImapdConfigVolumeMount());
+ }
+
+ StringBuilder args = new StringBuilder();
+ args.append("chown -R ");
+ args.append(GERRIT_FS_UID);
+ args.append(":");
+ args.append(GERRIT_FS_GID);
+ args.append(" ");
+ for (VolumeMount vm : volumeMounts) {
+ args.append(vm.getMountPath());
+ args.append(" ");
+ }
+
+ return new ContainerBuilder()
+ .withName("nfs-init")
+ .withImagePullPolicy(imageConfig.getImagePullPolicy())
+ .withImage(imageConfig.getBusyBox().getBusyBoxImage())
+ .withCommand(List.of("sh", "-c"))
+ .withArgs(args.toString().trim())
+ .withEnv(getPodNameEnvVar())
+ .withVolumeMounts(volumeMounts)
+ .build();
+ }
+
+ @JsonIgnore
+ public static EnvVar getPodNameEnvVar() {
+ return new EnvVarBuilder()
+ .withName("POD_NAME")
+ .withNewValueFrom()
+ .withNewFieldRef()
+ .withFieldPath("metadata.name")
+ .endFieldRef()
+ .endValueFrom()
+ .build();
+ }
+
+ @JsonIgnore
+ public String getDependentResourceName(String nameSuffix) {
+ return String.format("%s-%s", getMetadata().getName(), nameSuffix);
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/cluster/GerritClusterSpec.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/cluster/GerritClusterSpec.java
new file mode 100644
index 0000000..956c678
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/cluster/GerritClusterSpec.java
@@ -0,0 +1,91 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.cluster;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplate;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.ReceiverTemplate;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.ContainerImageConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GerritClusterIngressConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GerritStorageConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GlobalRefDbConfig;
+import java.util.ArrayList;
+import java.util.List;
+
+public class GerritClusterSpec {
+
+ private GerritStorageConfig storage = new GerritStorageConfig();
+ private ContainerImageConfig containerImages = new ContainerImageConfig();
+ private GerritClusterIngressConfig ingress = new GerritClusterIngressConfig();
+ private GlobalRefDbConfig refdb = new GlobalRefDbConfig();
+ private String serverId = "";
+ private List<GerritTemplate> gerrits = new ArrayList<>();
+ private ReceiverTemplate receiver;
+
+ public GerritStorageConfig getStorage() {
+ return storage;
+ }
+
+ public void setStorage(GerritStorageConfig storage) {
+ this.storage = storage;
+ }
+
+ public ContainerImageConfig getContainerImages() {
+ return containerImages;
+ }
+
+ public void setContainerImages(ContainerImageConfig containerImages) {
+ this.containerImages = containerImages;
+ }
+
+ public GerritClusterIngressConfig getIngress() {
+ return ingress;
+ }
+
+ public void setIngress(GerritClusterIngressConfig ingress) {
+ this.ingress = ingress;
+ }
+
+ public GlobalRefDbConfig getRefdb() {
+ return refdb;
+ }
+
+ public void setRefdb(GlobalRefDbConfig refdb) {
+ this.refdb = refdb;
+ }
+
+ public String getServerId() {
+ return serverId;
+ }
+
+ public void setServerId(String serverId) {
+ this.serverId = serverId;
+ }
+
+ public List<GerritTemplate> getGerrits() {
+ return gerrits;
+ }
+
+ public void setGerrits(List<GerritTemplate> gerrits) {
+ this.gerrits = gerrits;
+ }
+
+ public ReceiverTemplate getReceiver() {
+ return receiver;
+ }
+
+ public void setReceiver(ReceiverTemplate receiver) {
+ this.receiver = receiver;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/cluster/GerritClusterStatus.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/cluster/GerritClusterStatus.java
new file mode 100644
index 0000000..a0b853d
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/cluster/GerritClusterStatus.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.cluster;
+
+import java.util.List;
+import java.util.Map;
+
+public class GerritClusterStatus {
+ private Map<String, List<String>> members;
+
+ public Map<String, List<String>> getMembers() {
+ return members;
+ }
+
+ public void setMembers(Map<String, List<String>> members) {
+ this.members = members;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/Gerrit.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/Gerrit.java
new file mode 100644
index 0000000..4027cdd
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/Gerrit.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import io.fabric8.kubernetes.api.model.Namespaced;
+import io.fabric8.kubernetes.client.CustomResource;
+import io.fabric8.kubernetes.model.annotation.Group;
+import io.fabric8.kubernetes.model.annotation.ShortNames;
+import io.fabric8.kubernetes.model.annotation.Version;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+@Group("gerritoperator.google.com")
+@Version("v1alpha17")
+@ShortNames("gcr")
+public class Gerrit extends CustomResource<GerritSpec, GerritStatus> implements Namespaced {
+ private static final long serialVersionUID = 2L;
+
+ public String toString() {
+ return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE);
+ }
+
+ @JsonIgnore
+ public boolean isSshEnabled() {
+ return getSpec().getService().getSshPort() > 0;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritDebugConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritDebugConfig.java
new file mode 100644
index 0000000..7e7a5fd
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritDebugConfig.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit;
+
+public class GerritDebugConfig {
+ private boolean enabled;
+ private boolean suspend;
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public boolean isSuspend() {
+ return suspend;
+ }
+
+ public void setSuspend(boolean suspend) {
+ this.suspend = suspend;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritInitConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritInitConfig.java
new file mode 100644
index 0000000..82dc41f
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritInitConfig.java
@@ -0,0 +1,89 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
+
+public class GerritInitConfig {
+ private String caCertPath = "/var/config/ca.crt";
+ private boolean pluginCacheEnabled;
+ private String pluginCacheDir = "/var/mnt/plugins";
+ private List<GerritPlugin> plugins;
+ private List<GerritModule> libs;
+
+ @JsonProperty("highAvailability")
+ private boolean isHighlyAvailable;
+
+ private String refdb;
+
+ public String getCaCertPath() {
+ return caCertPath;
+ }
+
+ public void setCaCertPath(String caCertPath) {
+ this.caCertPath = caCertPath;
+ }
+
+ public boolean isPluginCacheEnabled() {
+ return pluginCacheEnabled;
+ }
+
+ public void setPluginCacheEnabled(boolean pluginCacheEnabled) {
+ this.pluginCacheEnabled = pluginCacheEnabled;
+ }
+
+ public String getPluginCacheDir() {
+ return pluginCacheDir;
+ }
+
+ public void setPluginCacheDir(String pluginCacheDir) {
+ this.pluginCacheDir = pluginCacheDir;
+ }
+
+ public List<GerritPlugin> getPlugins() {
+ return plugins;
+ }
+
+ public void setPlugins(List<GerritPlugin> plugins) {
+ this.plugins = plugins;
+ }
+
+ public List<GerritModule> getLibs() {
+ return libs;
+ }
+
+ public void setLibs(List<GerritModule> libs) {
+ this.libs = libs;
+ }
+
+ @JsonProperty("highAvailability")
+ public boolean isHighlyAvailable() {
+ return isHighlyAvailable;
+ }
+
+ @JsonProperty("highAvailability")
+ public void setHighlyAvailable(boolean isHighlyAvailable) {
+ this.isHighlyAvailable = isHighlyAvailable;
+ }
+
+ public String getRefdb() {
+ return refdb;
+ }
+
+ public void setRefdb(String refdb) {
+ this.refdb = refdb;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritModule.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritModule.java
new file mode 100644
index 0000000..5b0f241
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritModule.java
@@ -0,0 +1,66 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import java.io.Serializable;
+
+public class GerritModule implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ private String name;
+
+ @JsonInclude(JsonInclude.Include.NON_EMPTY)
+ private String url;
+
+ @JsonInclude(JsonInclude.Include.NON_EMPTY)
+ private String sha1;
+
+ public GerritModule() {}
+
+ public GerritModule(String name) {
+ this.name = name;
+ }
+
+ public GerritModule(String name, String url, String sha1) {
+ this.name = name;
+ this.url = url;
+ this.sha1 = sha1;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public String getSha1() {
+ return sha1;
+ }
+
+ public void setSha1(String sha1) {
+ this.sha1 = sha1;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritPlugin.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritPlugin.java
new file mode 100644
index 0000000..196ecb4
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritPlugin.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+public class GerritPlugin extends GerritModule {
+ private static final long serialVersionUID = 1L;
+
+ @JsonInclude(JsonInclude.Include.NON_EMPTY)
+ private boolean installAsLibrary = false;
+
+ public GerritPlugin() {}
+
+ public GerritPlugin(String name) {
+ super(name);
+ }
+
+ public GerritPlugin(String name, String url, String sha1) {
+ super(name, url, sha1);
+ }
+
+ public boolean isInstallAsLibrary() {
+ return installAsLibrary;
+ }
+
+ public void setInstallAsLibrary(boolean installAsLibrary) {
+ this.installAsLibrary = installAsLibrary;
+ }
+
+ @JsonIgnore
+ public boolean isPackagedPlugin() {
+ return getUrl() == null;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritProbe.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritProbe.java
new file mode 100644
index 0000000..46733ed
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritProbe.java
@@ -0,0 +1,81 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.google.gerrit.k8s.operator.gerrit.dependent.GerritStatefulSet;
+import io.fabric8.kubernetes.api.model.ExecAction;
+import io.fabric8.kubernetes.api.model.GRPCAction;
+import io.fabric8.kubernetes.api.model.HTTPGetAction;
+import io.fabric8.kubernetes.api.model.HTTPGetActionBuilder;
+import io.fabric8.kubernetes.api.model.IntOrString;
+import io.fabric8.kubernetes.api.model.Probe;
+import io.fabric8.kubernetes.api.model.TCPSocketAction;
+
+public class GerritProbe extends Probe {
+ private static final long serialVersionUID = 1L;
+
+ private static final HTTPGetAction HTTP_GET_ACTION =
+ new HTTPGetActionBuilder()
+ .withPath("/config/server/healthcheck~status")
+ .withPort(new IntOrString(GerritStatefulSet.HTTP_PORT))
+ .build();
+
+ @JsonIgnore private ExecAction exec;
+
+ @JsonIgnore private GRPCAction grpc;
+
+ @JsonIgnore private TCPSocketAction tcpSocket;
+
+ @Override
+ public void setExec(ExecAction exec) {
+ super.setExec(null);
+ }
+
+ @Override
+ public void setGrpc(GRPCAction grpc) {
+ super.setGrpc(null);
+ }
+
+ @Override
+ public void setHttpGet(HTTPGetAction httpGet) {
+ super.setHttpGet(HTTP_GET_ACTION);
+ }
+
+ @Override
+ public void setTcpSocket(TCPSocketAction tcpSocket) {
+ super.setTcpSocket(null);
+ }
+
+ @Override
+ public ExecAction getExec() {
+ return null;
+ }
+
+ @Override
+ public GRPCAction getGrpc() {
+ return null;
+ }
+
+ @Override
+ public HTTPGetAction getHttpGet() {
+ return HTTP_GET_ACTION;
+ }
+
+ @Override
+ public TCPSocketAction getTcpSocket() {
+ return null;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritSite.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritSite.java
new file mode 100644
index 0000000..2b604c8
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritSite.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit;
+
+import io.fabric8.kubernetes.api.model.Quantity;
+import java.io.Serializable;
+
+public class GerritSite implements Serializable {
+ private static final long serialVersionUID = 1L;
+ Quantity size;
+
+ public Quantity getSize() {
+ return size;
+ }
+
+ public void setSize(Quantity size) {
+ this.size = size;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritSpec.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritSpec.java
new file mode 100644
index 0000000..ed21087
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritSpec.java
@@ -0,0 +1,74 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.ContainerImageConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GerritStorageConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GlobalRefDbConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.IngressConfig;
+
+public class GerritSpec extends GerritTemplateSpec {
+ private ContainerImageConfig containerImages = new ContainerImageConfig();
+ private GerritStorageConfig storage = new GerritStorageConfig();
+ private IngressConfig ingress = new IngressConfig();
+ private GlobalRefDbConfig refdb = new GlobalRefDbConfig();
+ private String serverId = "";
+
+ public GerritSpec() {}
+
+ public GerritSpec(GerritTemplateSpec templateSpec) {
+ super(templateSpec);
+ }
+
+ public ContainerImageConfig getContainerImages() {
+ return containerImages;
+ }
+
+ public void setContainerImages(ContainerImageConfig containerImages) {
+ this.containerImages = containerImages;
+ }
+
+ public GerritStorageConfig getStorage() {
+ return storage;
+ }
+
+ public void setStorage(GerritStorageConfig storage) {
+ this.storage = storage;
+ }
+
+ public IngressConfig getIngress() {
+ return ingress;
+ }
+
+ public void setIngress(IngressConfig ingress) {
+ this.ingress = ingress;
+ }
+
+ public GlobalRefDbConfig getRefdb() {
+ return refdb;
+ }
+
+ public void setRefdb(GlobalRefDbConfig refdb) {
+ this.refdb = refdb;
+ }
+
+ public String getServerId() {
+ return serverId;
+ }
+
+ public void setServerId(String serverId) {
+ this.serverId = serverId;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritStatus.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritStatus.java
new file mode 100644
index 0000000..f779f75
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritStatus.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class GerritStatus {
+ private boolean ready = false;
+ private Map<String, String> appliedConfigMapVersions = new HashMap<>();
+ private Map<String, String> appliedSecretVersions = new HashMap<>();
+
+ public boolean isReady() {
+ return ready;
+ }
+
+ public void setReady(boolean ready) {
+ this.ready = ready;
+ }
+
+ public Map<String, String> getAppliedConfigMapVersions() {
+ return appliedConfigMapVersions;
+ }
+
+ public void setAppliedConfigMapVersions(Map<String, String> appliedConfigMapVersions) {
+ this.appliedConfigMapVersions = appliedConfigMapVersions;
+ }
+
+ public Map<String, String> getAppliedSecretVersions() {
+ return appliedSecretVersions;
+ }
+
+ public void setAppliedSecretVersions(Map<String, String> appliedSecretVersions) {
+ this.appliedSecretVersions = appliedSecretVersions;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritTemplate.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritTemplate.java
new file mode 100644
index 0000000..1144737
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritTemplate.java
@@ -0,0 +1,108 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GlobalRefDbConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.IngressConfig;
+import io.fabric8.kubernetes.api.model.KubernetesResource;
+import io.fabric8.kubernetes.api.model.ObjectMeta;
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+
+@JsonDeserialize(using = com.fasterxml.jackson.databind.JsonDeserializer.None.class)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonPropertyOrder({"metadata", "spec"})
+public class GerritTemplate implements KubernetesResource {
+ private static final long serialVersionUID = 1L;
+
+ @JsonProperty("metadata")
+ private ObjectMeta metadata;
+
+ @JsonProperty("spec")
+ private GerritTemplateSpec spec;
+
+ public GerritTemplate() {}
+
+ @JsonProperty("metadata")
+ public ObjectMeta getMetadata() {
+ return metadata;
+ }
+
+ @JsonProperty("metadata")
+ public void setMetadata(ObjectMeta metadata) {
+ this.metadata = metadata;
+ }
+
+ @JsonProperty("spec")
+ public GerritTemplateSpec getSpec() {
+ return spec;
+ }
+
+ @JsonProperty("spec")
+ public void setSpec(GerritTemplateSpec spec) {
+ this.spec = spec;
+ }
+
+ @JsonIgnore
+ public Gerrit toGerrit(GerritCluster gerritCluster) {
+ Gerrit gerrit = new Gerrit();
+ gerrit.setMetadata(getGerritMetadata(gerritCluster));
+ GerritSpec gerritSpec = new GerritSpec(spec);
+ gerritSpec.setContainerImages(gerritCluster.getSpec().getContainerImages());
+ gerritSpec.setStorage(gerritCluster.getSpec().getStorage());
+ IngressConfig ingressConfig = new IngressConfig();
+ ingressConfig.setEnabled(gerritCluster.getSpec().getIngress().isEnabled());
+ ingressConfig.setHost(gerritCluster.getSpec().getIngress().getHost());
+ ingressConfig.setTlsEnabled(gerritCluster.getSpec().getIngress().getTls().isEnabled());
+ ingressConfig.setSsh(gerritCluster.getSpec().getIngress().getSsh());
+ gerritSpec.setIngress(ingressConfig);
+ gerritSpec.setServerId(getServerId(gerritCluster));
+ if (getSpec().isHighlyAvailablePrimary()) {
+ GlobalRefDbConfig refdb = gerritCluster.getSpec().getRefdb();
+ if (refdb.getZookeeper() != null && refdb.getZookeeper().getRootNode() == null) {
+ refdb
+ .getZookeeper()
+ .setRootNode(
+ gerritCluster.getMetadata().getNamespace()
+ + "/"
+ + gerritCluster.getMetadata().getName());
+ }
+ gerritSpec.setRefdb(gerritCluster.getSpec().getRefdb());
+ }
+ gerrit.setSpec(gerritSpec);
+ return gerrit;
+ }
+
+ @JsonIgnore
+ private ObjectMeta getGerritMetadata(GerritCluster gerritCluster) {
+ return new ObjectMetaBuilder()
+ .withName(metadata.getName())
+ .withLabels(metadata.getLabels())
+ .withNamespace(gerritCluster.getMetadata().getNamespace())
+ .build();
+ }
+
+ private String getServerId(GerritCluster gerritCluster) {
+ String serverId = gerritCluster.getSpec().getServerId();
+ return serverId.isBlank()
+ ? gerritCluster.getMetadata().getNamespace() + "/" + gerritCluster.getMetadata().getName()
+ : serverId;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritTemplateSpec.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritTemplateSpec.java
new file mode 100644
index 0000000..4349cd1
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gerrit/GerritTemplateSpec.java
@@ -0,0 +1,259 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.HttpSshServiceConfig;
+import io.fabric8.kubernetes.api.model.Affinity;
+import io.fabric8.kubernetes.api.model.ResourceRequirements;
+import io.fabric8.kubernetes.api.model.Toleration;
+import io.fabric8.kubernetes.api.model.TopologySpreadConstraint;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class GerritTemplateSpec {
+ private String serviceAccount;
+
+ private List<Toleration> tolerations;
+ private Affinity affinity;
+ private List<TopologySpreadConstraint> topologySpreadConstraints = new ArrayList<>();
+ private String priorityClassName;
+
+ private int replicas = 1;
+ private int updatePartition = 0;
+
+ private ResourceRequirements resources;
+
+ private GerritProbe startupProbe = new GerritProbe();
+ private GerritProbe readinessProbe = new GerritProbe();
+ private GerritProbe livenessProbe = new GerritProbe();
+
+ private long gracefulStopTimeout = 30L;
+
+ private HttpSshServiceConfig service = new HttpSshServiceConfig();
+
+ private GerritSite site = new GerritSite();
+ private List<GerritPlugin> plugins = List.of();
+ private List<GerritModule> libs = List.of();
+ private Map<String, String> configFiles = Map.of();
+ private String secretRef;
+ private GerritMode mode = GerritMode.PRIMARY;
+
+ private GerritDebugConfig debug = new GerritDebugConfig();
+
+ public GerritTemplateSpec() {}
+
+ public GerritTemplateSpec(GerritTemplateSpec templateSpec) {
+ this.serviceAccount = templateSpec.serviceAccount;
+ this.tolerations = templateSpec.tolerations;
+ this.affinity = templateSpec.affinity;
+ this.topologySpreadConstraints = templateSpec.topologySpreadConstraints;
+ this.priorityClassName = templateSpec.priorityClassName;
+
+ this.replicas = templateSpec.replicas;
+ this.updatePartition = templateSpec.updatePartition;
+
+ this.resources = templateSpec.resources;
+
+ this.startupProbe = templateSpec.startupProbe;
+ this.readinessProbe = templateSpec.readinessProbe;
+ this.livenessProbe = templateSpec.livenessProbe;
+
+ this.gracefulStopTimeout = templateSpec.gracefulStopTimeout;
+
+ this.service = templateSpec.service;
+
+ this.site = templateSpec.site;
+ this.plugins = templateSpec.plugins;
+ this.libs = templateSpec.libs;
+ this.configFiles = templateSpec.configFiles;
+ this.secretRef = templateSpec.secretRef;
+ this.mode = templateSpec.mode;
+
+ this.debug = templateSpec.debug;
+ }
+
+ public String getServiceAccount() {
+ return serviceAccount;
+ }
+
+ public void setServiceAccount(String serviceAccount) {
+ this.serviceAccount = serviceAccount;
+ }
+
+ public List<Toleration> getTolerations() {
+ return tolerations;
+ }
+
+ public void setTolerations(List<Toleration> tolerations) {
+ this.tolerations = tolerations;
+ }
+
+ public Affinity getAffinity() {
+ return affinity;
+ }
+
+ public void setAffinity(Affinity affinity) {
+ this.affinity = affinity;
+ }
+
+ public List<TopologySpreadConstraint> getTopologySpreadConstraints() {
+ return topologySpreadConstraints;
+ }
+
+ public void setTopologySpreadConstraints(
+ List<TopologySpreadConstraint> topologySpreadConstraints) {
+ this.topologySpreadConstraints = topologySpreadConstraints;
+ }
+
+ public String getPriorityClassName() {
+ return priorityClassName;
+ }
+
+ public void setPriorityClassName(String priorityClassName) {
+ this.priorityClassName = priorityClassName;
+ }
+
+ public int getReplicas() {
+ return replicas;
+ }
+
+ public void setReplicas(int replicas) {
+ this.replicas = replicas;
+ }
+
+ public int getUpdatePartition() {
+ return updatePartition;
+ }
+
+ public void setUpdatePartition(int updatePartition) {
+ this.updatePartition = updatePartition;
+ }
+
+ public ResourceRequirements getResources() {
+ return resources;
+ }
+
+ public void setResources(ResourceRequirements resources) {
+ this.resources = resources;
+ }
+
+ public GerritProbe getStartupProbe() {
+ return startupProbe;
+ }
+
+ public void setStartupProbe(GerritProbe startupProbe) {
+ this.startupProbe = startupProbe;
+ }
+
+ public GerritProbe getReadinessProbe() {
+ return readinessProbe;
+ }
+
+ public void setReadinessProbe(GerritProbe readinessProbe) {
+ this.readinessProbe = readinessProbe;
+ }
+
+ public GerritProbe getLivenessProbe() {
+ return livenessProbe;
+ }
+
+ public void setLivenessProbe(GerritProbe livenessProbe) {
+ this.livenessProbe = livenessProbe;
+ }
+
+ public long getGracefulStopTimeout() {
+ return gracefulStopTimeout;
+ }
+
+ public void setGracefulStopTimeout(long gracefulStopTimeout) {
+ this.gracefulStopTimeout = gracefulStopTimeout;
+ }
+
+ public HttpSshServiceConfig getService() {
+ return service;
+ }
+
+ public void setService(HttpSshServiceConfig service) {
+ this.service = service;
+ }
+
+ public GerritSite getSite() {
+ return site;
+ }
+
+ public void setSite(GerritSite site) {
+ this.site = site;
+ }
+
+ public List<GerritPlugin> getPlugins() {
+ return plugins;
+ }
+
+ public void setPlugins(List<GerritPlugin> plugins) {
+ this.plugins = plugins;
+ }
+
+ public List<GerritModule> getLibs() {
+ return libs;
+ }
+
+ public void setLibs(List<GerritModule> libs) {
+ this.libs = libs;
+ }
+
+ public Map<String, String> getConfigFiles() {
+ return configFiles;
+ }
+
+ public void setConfigFiles(Map<String, String> configFiles) {
+ this.configFiles = configFiles;
+ }
+
+ public String getSecretRef() {
+ return secretRef;
+ }
+
+ public void setSecretRef(String secretRef) {
+ this.secretRef = secretRef;
+ }
+
+ public GerritMode getMode() {
+ return mode;
+ }
+
+ public void setMode(GerritMode mode) {
+ this.mode = mode;
+ }
+
+ public GerritDebugConfig getDebug() {
+ return debug;
+ }
+
+ public void setDebug(GerritDebugConfig debug) {
+ this.debug = debug;
+ }
+
+ public enum GerritMode {
+ PRIMARY,
+ REPLICA
+ }
+
+ @JsonIgnore
+ public boolean isHighlyAvailablePrimary() {
+ return getMode().equals(GerritMode.PRIMARY) && getReplicas() > 1;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gitgc/GitGarbageCollection.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gitgc/GitGarbageCollection.java
new file mode 100644
index 0000000..7d268bd
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gitgc/GitGarbageCollection.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.gitgc;
+
+import io.fabric8.kubernetes.api.model.Namespaced;
+import io.fabric8.kubernetes.client.CustomResource;
+import io.fabric8.kubernetes.model.annotation.Group;
+import io.fabric8.kubernetes.model.annotation.Plural;
+import io.fabric8.kubernetes.model.annotation.ShortNames;
+import io.fabric8.kubernetes.model.annotation.Version;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+@Group("gerritoperator.google.com")
+@Version("v1alpha1")
+@ShortNames("gitgc")
+@Plural("gitgcs")
+public class GitGarbageCollection
+ extends CustomResource<GitGarbageCollectionSpec, GitGarbageCollectionStatus>
+ implements Namespaced {
+ private static final long serialVersionUID = 1L;
+
+ public String toString() {
+ return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE);
+ }
+
+ @Override
+ protected GitGarbageCollectionStatus initStatus() {
+ return new GitGarbageCollectionStatus();
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gitgc/GitGarbageCollectionSpec.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gitgc/GitGarbageCollectionSpec.java
new file mode 100644
index 0000000..7648f13
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gitgc/GitGarbageCollectionSpec.java
@@ -0,0 +1,102 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.gitgc;
+
+import io.fabric8.kubernetes.api.model.Affinity;
+import io.fabric8.kubernetes.api.model.ResourceRequirements;
+import io.fabric8.kubernetes.api.model.Toleration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+public class GitGarbageCollectionSpec {
+ private String cluster;
+ private String schedule;
+ private Set<String> projects;
+ private ResourceRequirements resources;
+ private List<Toleration> tolerations;
+ private Affinity affinity;
+
+ public GitGarbageCollectionSpec() {
+ resources = new ResourceRequirements();
+ projects = new HashSet<>();
+ }
+
+ public String getCluster() {
+ return cluster;
+ }
+
+ public void setCluster(String cluster) {
+ this.cluster = cluster;
+ }
+
+ public void setSchedule(String schedule) {
+ this.schedule = schedule;
+ }
+
+ public String getSchedule() {
+ return schedule;
+ }
+
+ public Set<String> getProjects() {
+ return projects;
+ }
+
+ public void setProjects(Set<String> projects) {
+ this.projects = projects;
+ }
+
+ public void setResources(ResourceRequirements resources) {
+ this.resources = resources;
+ }
+
+ public ResourceRequirements getResources() {
+ return resources;
+ }
+
+ public List<Toleration> getTolerations() {
+ return tolerations;
+ }
+
+ public void setTolerations(List<Toleration> tolerations) {
+ this.tolerations = tolerations;
+ }
+
+ public Affinity getAffinity() {
+ return affinity;
+ }
+
+ public void setAffinity(Affinity affinity) {
+ this.affinity = affinity;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(cluster, projects, resources, schedule);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof GitGarbageCollectionSpec) {
+ GitGarbageCollectionSpec other = (GitGarbageCollectionSpec) obj;
+ return Objects.equals(cluster, other.cluster)
+ && Objects.equals(projects, other.projects)
+ && Objects.equals(resources, other.resources)
+ && Objects.equals(schedule, other.schedule);
+ }
+ return false;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gitgc/GitGarbageCollectionStatus.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gitgc/GitGarbageCollectionStatus.java
new file mode 100644
index 0000000..ddff4c7
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/gitgc/GitGarbageCollectionStatus.java
@@ -0,0 +1,76 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.gitgc;
+
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+public class GitGarbageCollectionStatus {
+ private boolean replicateAll = false;
+ private Set<String> excludedProjects = new HashSet<>();
+ private GitGcState state = GitGcState.INACTIVE;
+
+ public boolean isReplicateAll() {
+ return replicateAll;
+ }
+
+ public void setReplicateAll(boolean replicateAll) {
+ this.replicateAll = replicateAll;
+ }
+
+ public Set<String> getExcludedProjects() {
+ return excludedProjects;
+ }
+
+ public void resetExcludedProjects() {
+ excludedProjects = new HashSet<>();
+ }
+
+ public void excludeProjects(Set<String> projects) {
+ excludedProjects.addAll(projects);
+ }
+
+ public GitGcState getState() {
+ return state;
+ }
+
+ public void setState(GitGcState state) {
+ this.state = state;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(excludedProjects, replicateAll, state);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof GitGarbageCollectionStatus) {
+ GitGarbageCollectionStatus other = (GitGarbageCollectionStatus) obj;
+ return Objects.equals(excludedProjects, other.excludedProjects)
+ && replicateAll == other.replicateAll
+ && state == other.state;
+ }
+ return false;
+ }
+
+ public enum GitGcState {
+ ACTIVE,
+ INACTIVE,
+ CONFLICT,
+ ERROR
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/network/GerritNetwork.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/network/GerritNetwork.java
new file mode 100644
index 0000000..7596f54
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/network/GerritNetwork.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.network;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import io.fabric8.kubernetes.api.model.Namespaced;
+import io.fabric8.kubernetes.api.model.Status;
+import io.fabric8.kubernetes.client.CustomResource;
+import io.fabric8.kubernetes.model.annotation.Group;
+import io.fabric8.kubernetes.model.annotation.ShortNames;
+import io.fabric8.kubernetes.model.annotation.Version;
+
+@Group("gerritoperator.google.com")
+@Version("v1alpha2")
+@ShortNames("gn")
+public class GerritNetwork extends CustomResource<GerritNetworkSpec, Status> implements Namespaced {
+ private static final long serialVersionUID = 1L;
+
+ public static final String SESSION_COOKIE_NAME = "Gerrit_Session";
+ public static final String SESSION_COOKIE_TTL = "60s";
+
+ @JsonIgnore
+ public String getDependentResourceName(String nameSuffix) {
+ return String.format("%s-%s", getMetadata().getName(), nameSuffix);
+ }
+
+ @JsonIgnore
+ public boolean hasPrimaryGerrit() {
+ return getSpec().getPrimaryGerrit() != null;
+ }
+
+ @JsonIgnore
+ public boolean hasGerritReplica() {
+ return getSpec().getGerritReplica() != null;
+ }
+
+ @JsonIgnore
+ public boolean hasGerrits() {
+ return hasGerritReplica() || hasPrimaryGerrit();
+ }
+
+ @JsonIgnore
+ public boolean hasReceiver() {
+ return getSpec().getReceiver() != null;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/network/GerritNetworkSpec.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/network/GerritNetworkSpec.java
new file mode 100644
index 0000000..406341f
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/network/GerritNetworkSpec.java
@@ -0,0 +1,69 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.network;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GerritClusterIngressConfig;
+import java.util.ArrayList;
+import java.util.List;
+
+public class GerritNetworkSpec {
+ private GerritClusterIngressConfig ingress = new GerritClusterIngressConfig();
+ private NetworkMember receiver;
+ private NetworkMemberWithSsh primaryGerrit;
+ private NetworkMemberWithSsh gerritReplica;
+
+ public GerritClusterIngressConfig getIngress() {
+ return ingress;
+ }
+
+ public void setIngress(GerritClusterIngressConfig ingress) {
+ this.ingress = ingress;
+ }
+
+ public NetworkMember getReceiver() {
+ return receiver;
+ }
+
+ public void setReceiver(NetworkMember receiver) {
+ this.receiver = receiver;
+ }
+
+ public NetworkMemberWithSsh getPrimaryGerrit() {
+ return primaryGerrit;
+ }
+
+ public void setPrimaryGerrit(NetworkMemberWithSsh primaryGerrit) {
+ this.primaryGerrit = primaryGerrit;
+ }
+
+ public NetworkMemberWithSsh getGerritReplica() {
+ return gerritReplica;
+ }
+
+ public void setGerritReplica(NetworkMemberWithSsh gerritReplica) {
+ this.gerritReplica = gerritReplica;
+ }
+
+ public List<NetworkMemberWithSsh> getGerrits() {
+ List<NetworkMemberWithSsh> gerrits = new ArrayList<>();
+ if (primaryGerrit != null) {
+ gerrits.add(primaryGerrit);
+ }
+ if (gerritReplica != null) {
+ gerrits.add(gerritReplica);
+ }
+ return gerrits;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/network/NetworkMember.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/network/NetworkMember.java
new file mode 100644
index 0000000..433dbbe
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/network/NetworkMember.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.network;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.HttpServiceConfig;
+
+public class NetworkMember {
+ private String name;
+ private int httpPort = 8080;
+
+ public NetworkMember() {}
+
+ public NetworkMember(String name, HttpServiceConfig serviceConfig) {
+ this.name = name;
+ this.httpPort = serviceConfig.getHttpPort();
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public int getHttpPort() {
+ return httpPort;
+ }
+
+ public void setHttpPort(int httpPort) {
+ this.httpPort = httpPort;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/network/NetworkMemberWithSsh.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/network/NetworkMemberWithSsh.java
new file mode 100644
index 0000000..1668739
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/network/NetworkMemberWithSsh.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.network;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.HttpSshServiceConfig;
+
+public class NetworkMemberWithSsh extends NetworkMember {
+ private int sshPort = 29418;
+
+ public NetworkMemberWithSsh() {}
+
+ public NetworkMemberWithSsh(String name, HttpSshServiceConfig serviceConfig) {
+ super(name, serviceConfig);
+ this.sshPort = serviceConfig.getSshPort();
+ }
+
+ public int getSshPort() {
+ return sshPort;
+ }
+
+ public void setSshPort(int sshPort) {
+ this.sshPort = sshPort;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/Receiver.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/Receiver.java
new file mode 100644
index 0000000..bc546df
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/Receiver.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.receiver;
+
+import io.fabric8.kubernetes.api.model.Namespaced;
+import io.fabric8.kubernetes.client.CustomResource;
+import io.fabric8.kubernetes.model.annotation.Group;
+import io.fabric8.kubernetes.model.annotation.ShortNames;
+import io.fabric8.kubernetes.model.annotation.Version;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+@Group("gerritoperator.google.com")
+@Version("v1alpha6")
+@ShortNames("grec")
+public class Receiver extends CustomResource<ReceiverSpec, ReceiverStatus> implements Namespaced {
+ private static final long serialVersionUID = 1L;
+
+ public String toString() {
+ return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE);
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/ReceiverProbe.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/ReceiverProbe.java
new file mode 100644
index 0000000..78aaa58
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/ReceiverProbe.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.receiver;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.google.gerrit.k8s.operator.receiver.dependent.ReceiverDeployment;
+import io.fabric8.kubernetes.api.model.ExecAction;
+import io.fabric8.kubernetes.api.model.GRPCAction;
+import io.fabric8.kubernetes.api.model.HTTPGetAction;
+import io.fabric8.kubernetes.api.model.IntOrString;
+import io.fabric8.kubernetes.api.model.Probe;
+import io.fabric8.kubernetes.api.model.TCPSocketAction;
+import io.fabric8.kubernetes.api.model.TCPSocketActionBuilder;
+
+public class ReceiverProbe extends Probe {
+ private static final long serialVersionUID = 1L;
+
+ private static final TCPSocketAction TCP_SOCKET_ACTION =
+ new TCPSocketActionBuilder().withPort(new IntOrString(ReceiverDeployment.HTTP_PORT)).build();
+
+ @JsonIgnore private ExecAction exec;
+
+ @JsonIgnore private GRPCAction grpc;
+
+ @JsonIgnore private TCPSocketAction tcpSocket;
+
+ @Override
+ public void setExec(ExecAction exec) {
+ super.setExec(null);
+ }
+
+ @Override
+ public void setGrpc(GRPCAction grpc) {
+ super.setGrpc(null);
+ }
+
+ @Override
+ public void setHttpGet(HTTPGetAction httpGet) {
+ super.setHttpGet(null);
+ }
+
+ @Override
+ public void setTcpSocket(TCPSocketAction tcpSocket) {
+ super.setTcpSocket(TCP_SOCKET_ACTION);
+ }
+
+ @Override
+ public ExecAction getExec() {
+ return null;
+ }
+
+ @Override
+ public GRPCAction getGrpc() {
+ return null;
+ }
+
+ @Override
+ public HTTPGetAction getHttpGet() {
+ return null;
+ }
+
+ @Override
+ public TCPSocketAction getTcpSocket() {
+ return TCP_SOCKET_ACTION;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/ReceiverSpec.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/ReceiverSpec.java
new file mode 100644
index 0000000..005fa49
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/ReceiverSpec.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.receiver;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.ContainerImageConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.IngressConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.StorageConfig;
+
+public class ReceiverSpec extends ReceiverTemplateSpec {
+ private ContainerImageConfig containerImages = new ContainerImageConfig();
+ private StorageConfig storage = new StorageConfig();
+ private IngressConfig ingress = new IngressConfig();
+
+ public ReceiverSpec() {}
+
+ public ReceiverSpec(ReceiverTemplateSpec templateSpec) {
+ super(templateSpec);
+ }
+
+ public ContainerImageConfig getContainerImages() {
+ return containerImages;
+ }
+
+ public void setContainerImages(ContainerImageConfig containerImages) {
+ this.containerImages = containerImages;
+ }
+
+ public StorageConfig getStorage() {
+ return storage;
+ }
+
+ public void setStorage(StorageConfig storage) {
+ this.storage = storage;
+ }
+
+ public IngressConfig getIngress() {
+ return ingress;
+ }
+
+ public void setIngress(IngressConfig ingress) {
+ this.ingress = ingress;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/ReceiverStatus.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/ReceiverStatus.java
new file mode 100644
index 0000000..915a62f
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/ReceiverStatus.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.receiver;
+
+public class ReceiverStatus {
+ private boolean ready;
+ private String appliedCredentialSecretVersion = "";
+
+ public boolean isReady() {
+ return ready;
+ }
+
+ public void setReady(boolean ready) {
+ this.ready = ready;
+ }
+
+ public String getAppliedCredentialSecretVersion() {
+ return appliedCredentialSecretVersion;
+ }
+
+ public void setAppliedCredentialSecretVersion(String appliedCredentialSecretVersion) {
+ this.appliedCredentialSecretVersion = appliedCredentialSecretVersion;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/ReceiverTemplate.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/ReceiverTemplate.java
new file mode 100644
index 0000000..f559419
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/ReceiverTemplate.java
@@ -0,0 +1,86 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.receiver;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.IngressConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.StorageConfig;
+import io.fabric8.kubernetes.api.model.KubernetesResource;
+import io.fabric8.kubernetes.api.model.ObjectMeta;
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+
+@JsonDeserialize(using = com.fasterxml.jackson.databind.JsonDeserializer.None.class)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonPropertyOrder({"metadata", "spec"})
+public class ReceiverTemplate implements KubernetesResource {
+ private static final long serialVersionUID = 1L;
+
+ @JsonProperty("metadata")
+ private ObjectMeta metadata;
+
+ @JsonProperty("spec")
+ private ReceiverTemplateSpec spec;
+
+ public ReceiverTemplate() {}
+
+ @JsonProperty("metadata")
+ public ObjectMeta getMetadata() {
+ return metadata;
+ }
+
+ @JsonProperty("metadata")
+ public void setMetadata(ObjectMeta metadata) {
+ this.metadata = metadata;
+ }
+
+ @JsonProperty("spec")
+ public ReceiverTemplateSpec getSpec() {
+ return spec;
+ }
+
+ @JsonProperty("spec")
+ public void setSpec(ReceiverTemplateSpec spec) {
+ this.spec = spec;
+ }
+
+ @JsonIgnore
+ public Receiver toReceiver(GerritCluster gerritCluster) {
+ Receiver receiver = new Receiver();
+ receiver.setMetadata(getReceiverMetadata(gerritCluster));
+ ReceiverSpec receiverSpec = new ReceiverSpec(spec);
+ receiverSpec.setContainerImages(gerritCluster.getSpec().getContainerImages());
+ receiverSpec.setStorage(new StorageConfig(gerritCluster.getSpec().getStorage()));
+ IngressConfig ingressConfig = new IngressConfig();
+ ingressConfig.setHost(gerritCluster.getSpec().getIngress().getHost());
+ ingressConfig.setTlsEnabled(gerritCluster.getSpec().getIngress().getTls().isEnabled());
+ receiverSpec.setIngress(ingressConfig);
+ receiver.setSpec(receiverSpec);
+ return receiver;
+ }
+
+ @JsonIgnore
+ private ObjectMeta getReceiverMetadata(GerritCluster gerritCluster) {
+ return new ObjectMetaBuilder()
+ .withName(metadata.getName())
+ .withLabels(metadata.getLabels())
+ .withNamespace(gerritCluster.getMetadata().getNamespace())
+ .build();
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/ReceiverTemplateSpec.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/ReceiverTemplateSpec.java
new file mode 100644
index 0000000..22ee904
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/receiver/ReceiverTemplateSpec.java
@@ -0,0 +1,163 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.receiver;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.HttpServiceConfig;
+import io.fabric8.kubernetes.api.model.Affinity;
+import io.fabric8.kubernetes.api.model.IntOrString;
+import io.fabric8.kubernetes.api.model.ResourceRequirements;
+import io.fabric8.kubernetes.api.model.Toleration;
+import io.fabric8.kubernetes.api.model.TopologySpreadConstraint;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ReceiverTemplateSpec {
+ private List<Toleration> tolerations = new ArrayList<>();
+ private Affinity affinity;
+ private List<TopologySpreadConstraint> topologySpreadConstraints = new ArrayList<>();
+ private String priorityClassName;
+
+ private int replicas = 1;
+ private IntOrString maxSurge = new IntOrString(1);
+ private IntOrString maxUnavailable = new IntOrString(1);
+
+ private ResourceRequirements resources;
+
+ private ReceiverProbe readinessProbe = new ReceiverProbe();
+ private ReceiverProbe livenessProbe = new ReceiverProbe();
+
+ private HttpServiceConfig service = new HttpServiceConfig();
+
+ private String credentialSecretRef;
+
+ public ReceiverTemplateSpec() {}
+
+ public ReceiverTemplateSpec(ReceiverTemplateSpec templateSpec) {
+ this.tolerations = templateSpec.tolerations;
+ this.affinity = templateSpec.affinity;
+ this.topologySpreadConstraints = templateSpec.topologySpreadConstraints;
+ this.priorityClassName = templateSpec.priorityClassName;
+
+ this.replicas = templateSpec.replicas;
+
+ this.resources = templateSpec.resources;
+ this.maxSurge = templateSpec.maxSurge;
+ this.maxUnavailable = templateSpec.maxUnavailable;
+
+ this.readinessProbe = templateSpec.readinessProbe;
+ this.livenessProbe = templateSpec.livenessProbe;
+
+ this.service = templateSpec.service;
+
+ this.credentialSecretRef = templateSpec.credentialSecretRef;
+ }
+
+ public List<Toleration> getTolerations() {
+ return tolerations;
+ }
+
+ public void setTolerations(List<Toleration> tolerations) {
+ this.tolerations = tolerations;
+ }
+
+ public Affinity getAffinity() {
+ return affinity;
+ }
+
+ public void setAffinity(Affinity affinity) {
+ this.affinity = affinity;
+ }
+
+ public List<TopologySpreadConstraint> getTopologySpreadConstraints() {
+ return topologySpreadConstraints;
+ }
+
+ public void setTopologySpreadConstraints(
+ List<TopologySpreadConstraint> topologySpreadConstraints) {
+ this.topologySpreadConstraints = topologySpreadConstraints;
+ }
+
+ public String getPriorityClassName() {
+ return priorityClassName;
+ }
+
+ public void setPriorityClassName(String priorityClassName) {
+ this.priorityClassName = priorityClassName;
+ }
+
+ public int getReplicas() {
+ return replicas;
+ }
+
+ public void setReplicas(int replicas) {
+ this.replicas = replicas;
+ }
+
+ public IntOrString getMaxSurge() {
+ return maxSurge;
+ }
+
+ public void setMaxSurge(IntOrString maxSurge) {
+ this.maxSurge = maxSurge;
+ }
+
+ public IntOrString getMaxUnavailable() {
+ return maxUnavailable;
+ }
+
+ public void setMaxUnavailable(IntOrString maxUnavailable) {
+ this.maxUnavailable = maxUnavailable;
+ }
+
+ public ResourceRequirements getResources() {
+ return resources;
+ }
+
+ public void setResources(ResourceRequirements resources) {
+ this.resources = resources;
+ }
+
+ public ReceiverProbe getReadinessProbe() {
+ return readinessProbe;
+ }
+
+ public void setReadinessProbe(ReceiverProbe readinessProbe) {
+ this.readinessProbe = readinessProbe;
+ }
+
+ public ReceiverProbe getLivenessProbe() {
+ return livenessProbe;
+ }
+
+ public void setLivenessProbe(ReceiverProbe livenessProbe) {
+ this.livenessProbe = livenessProbe;
+ }
+
+ public HttpServiceConfig getService() {
+ return service;
+ }
+
+ public void setService(HttpServiceConfig service) {
+ this.service = service;
+ }
+
+ public String getCredentialSecretRef() {
+ return credentialSecretRef;
+ }
+
+ public void setCredentialSecretRef(String credentialSecretRef) {
+ this.credentialSecretRef = credentialSecretRef;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/BusyBoxImage.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/BusyBoxImage.java
new file mode 100644
index 0000000..5c5388f
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/BusyBoxImage.java
@@ -0,0 +1,62 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+public class BusyBoxImage {
+ private String registry;
+ private String tag;
+
+ public BusyBoxImage() {
+ this.registry = "docker.io";
+ this.tag = "latest";
+ }
+
+ public void setRegistry(String registry) {
+ this.registry = registry;
+ }
+
+ public String getRegistry() {
+ return registry;
+ }
+
+ public void setTag(String tag) {
+ this.tag = tag;
+ }
+
+ public String getTag() {
+ return tag;
+ }
+
+ @JsonIgnore
+ public String getBusyBoxImage() {
+ StringBuilder builder = new StringBuilder();
+
+ if (registry != null) {
+ builder.append(registry);
+ builder.append("/");
+ }
+
+ builder.append("busybox");
+
+ if (tag != null) {
+ builder.append(":");
+ builder.append(tag);
+ }
+
+ return builder.toString();
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/ContainerImageConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/ContainerImageConfig.java
new file mode 100644
index 0000000..4f84824
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/ContainerImageConfig.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+import io.fabric8.kubernetes.api.model.LocalObjectReference;
+import java.util.HashSet;
+import java.util.Set;
+
+public class ContainerImageConfig {
+ private String imagePullPolicy = "Always";
+ private Set<LocalObjectReference> imagePullSecrets = new HashSet<>();
+ private BusyBoxImage busyBox = new BusyBoxImage();
+ private GerritRepositoryConfig gerritImages = new GerritRepositoryConfig();
+
+ public String getImagePullPolicy() {
+ return imagePullPolicy;
+ }
+
+ public void setImagePullPolicy(String imagePullPolicy) {
+ this.imagePullPolicy = imagePullPolicy;
+ }
+
+ public Set<LocalObjectReference> getImagePullSecrets() {
+ return imagePullSecrets;
+ }
+
+ public void setImagePullSecrets(Set<LocalObjectReference> imagePullSecrets) {
+ this.imagePullSecrets = imagePullSecrets;
+ }
+
+ public BusyBoxImage getBusyBox() {
+ return busyBox;
+ }
+
+ public void setBusyBox(BusyBoxImage busyBox) {
+ this.busyBox = busyBox;
+ }
+
+ public GerritRepositoryConfig getGerritImages() {
+ return gerritImages;
+ }
+
+ public void setGerritImages(GerritRepositoryConfig gerritImages) {
+ this.gerritImages = gerritImages;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritClusterIngressConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritClusterIngressConfig.java
new file mode 100644
index 0000000..a915820
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritClusterIngressConfig.java
@@ -0,0 +1,85 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import java.util.Map;
+
+public class GerritClusterIngressConfig {
+ private boolean enabled = false;
+ private String host;
+ private Map<String, String> annotations;
+ private GerritIngressTlsConfig tls = new GerritIngressTlsConfig();
+ private GerritIngressSshConfig ssh = new GerritIngressSshConfig();
+ private GerritIngressAmbassadorConfig ambassador = new GerritIngressAmbassadorConfig();
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public String getHost() {
+ return host;
+ }
+
+ public void setHost(String host) {
+ this.host = host;
+ }
+
+ public Map<String, String> getAnnotations() {
+ return annotations;
+ }
+
+ public void setAnnotations(Map<String, String> annotations) {
+ this.annotations = annotations;
+ }
+
+ public GerritIngressTlsConfig getTls() {
+ return tls;
+ }
+
+ public void setTls(GerritIngressTlsConfig tls) {
+ this.tls = tls;
+ }
+
+ public GerritIngressSshConfig getSsh() {
+ return ssh;
+ }
+
+ public void setSsh(GerritIngressSshConfig ssh) {
+ this.ssh = ssh;
+ }
+
+ public GerritIngressAmbassadorConfig getAmbassador() {
+ return ambassador;
+ }
+
+ public void setAmbassador(GerritIngressAmbassadorConfig ambassador) {
+ this.ambassador = ambassador;
+ }
+
+ @JsonIgnore
+ public String getFullHostnameForService(String svcName) {
+ return getFullHostnameForService(svcName, getHost());
+ }
+
+ @JsonIgnore
+ public static String getFullHostnameForService(String svcName, String ingressHost) {
+ return String.format("%s.%s", svcName, ingressHost);
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritIngressAmbassadorConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritIngressAmbassadorConfig.java
new file mode 100644
index 0000000..b34eb67
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritIngressAmbassadorConfig.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+import java.util.List;
+
+public class GerritIngressAmbassadorConfig {
+ private List<String> id;
+ private boolean createHost;
+
+ public List<String> getId() {
+ return this.id;
+ }
+
+ public void setId(List<String> id) {
+ this.id = id;
+ }
+
+ public boolean getCreateHost() {
+ return this.createHost;
+ }
+
+ public void setCreateHost(boolean createHost) {
+ this.createHost = createHost;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritIngressSshConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritIngressSshConfig.java
new file mode 100644
index 0000000..6203803
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritIngressSshConfig.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+public class GerritIngressSshConfig {
+ private boolean enabled = false;
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritIngressTlsConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritIngressTlsConfig.java
new file mode 100644
index 0000000..b4552f5
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritIngressTlsConfig.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+public class GerritIngressTlsConfig {
+
+ private boolean enabled = false;
+ private String secret;
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public String getSecret() {
+ return secret;
+ }
+
+ public void setSecret(String secret) {
+ this.secret = secret;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritRepositoryConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritRepositoryConfig.java
new file mode 100644
index 0000000..f593c1e
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritRepositoryConfig.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+public class GerritRepositoryConfig {
+ private String registry;
+ private String org;
+ private String tag;
+
+ public GerritRepositoryConfig() {
+ this.registry = "docker.io";
+ this.org = "k8sgerrit";
+ this.tag = "latest";
+ }
+
+ public void setRegistry(String registry) {
+ this.registry = registry;
+ }
+
+ public String getRegistry() {
+ return registry;
+ }
+
+ public String getOrg() {
+ return org;
+ }
+
+ public void setOrg(String org) {
+ this.org = org;
+ }
+
+ public void setTag(String tag) {
+ this.tag = tag;
+ }
+
+ public String getTag() {
+ return tag;
+ }
+
+ @JsonIgnore
+ public String getFullImageName(String image) {
+ StringBuilder builder = new StringBuilder();
+
+ if (registry != null) {
+ builder.append(registry);
+ builder.append("/");
+ }
+
+ if (org != null) {
+ builder.append(org);
+ builder.append("/");
+ }
+
+ builder.append(image);
+
+ if (tag != null) {
+ builder.append(":");
+ builder.append(tag);
+ }
+
+ return builder.toString();
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritStorageConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritStorageConfig.java
new file mode 100644
index 0000000..d66a0cd
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GerritStorageConfig.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+public class GerritStorageConfig extends StorageConfig {
+ private PluginCacheConfig pluginCache = new PluginCacheConfig();
+
+ public PluginCacheConfig getPluginCache() {
+ return pluginCache;
+ }
+
+ public void setPluginCache(PluginCacheConfig pluginCache) {
+ this.pluginCache = pluginCache;
+ }
+
+ public class PluginCacheConfig {
+ private boolean enabled;
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GlobalRefDbConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GlobalRefDbConfig.java
new file mode 100644
index 0000000..d338555
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/GlobalRefDbConfig.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+public class GlobalRefDbConfig {
+ private RefDatabase database = RefDatabase.NONE;
+ private ZookeeperRefDbConfig zookeeper;
+ private SpannerRefDbConfig spanner;
+
+ public RefDatabase getDatabase() {
+ return database;
+ }
+
+ public void setDatabase(RefDatabase database) {
+ this.database = database;
+ }
+
+ public ZookeeperRefDbConfig getZookeeper() {
+ return zookeeper;
+ }
+
+ public void setZookeeper(ZookeeperRefDbConfig zookeeper) {
+ this.zookeeper = zookeeper;
+ }
+
+ public SpannerRefDbConfig getSpanner() {
+ return spanner;
+ }
+
+ public void setSpanner(SpannerRefDbConfig spanner) {
+ this.spanner = spanner;
+ }
+
+ public enum RefDatabase {
+ NONE,
+ ZOOKEEPER,
+ SPANNER,
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/HttpServiceConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/HttpServiceConfig.java
new file mode 100644
index 0000000..8e7d651
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/HttpServiceConfig.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+import java.io.Serializable;
+
+public class HttpServiceConfig implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ String type = "NodePort";
+ int httpPort = 80;
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public int getHttpPort() {
+ return httpPort;
+ }
+
+ public void setHttpPort(int httpPort) {
+ this.httpPort = httpPort;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/HttpSshServiceConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/HttpSshServiceConfig.java
new file mode 100644
index 0000000..8655c32
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/HttpSshServiceConfig.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+import java.io.Serializable;
+
+public class HttpSshServiceConfig extends HttpServiceConfig implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ int sshPort = 0;
+
+ public int getSshPort() {
+ return sshPort;
+ }
+
+ public void setSshPort(int sshPort) {
+ this.sshPort = sshPort;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/IngressConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/IngressConfig.java
new file mode 100644
index 0000000..f83b189
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/IngressConfig.java
@@ -0,0 +1,75 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+public class IngressConfig {
+ private boolean enabled;
+ private String host;
+ private boolean tlsEnabled;
+ private GerritIngressSshConfig ssh = new GerritIngressSshConfig();
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public String getHost() {
+ return host;
+ }
+
+ public void setHost(String host) {
+ this.host = host;
+ }
+
+ public boolean isTlsEnabled() {
+ return tlsEnabled;
+ }
+
+ public void setTlsEnabled(boolean tlsEnabled) {
+ this.tlsEnabled = tlsEnabled;
+ }
+
+ public GerritIngressSshConfig getSsh() {
+ return ssh;
+ }
+
+ public void setSsh(GerritIngressSshConfig ssh) {
+ this.ssh = ssh;
+ }
+
+ @JsonIgnore
+ public String getFullHostnameForService(String svcName) {
+ return String.format("%s.%s", svcName, getHost());
+ }
+
+ @JsonIgnore
+ public String getUrl() {
+ String protocol = isTlsEnabled() ? "https" : "http";
+ String hostname = getHost();
+ return String.format("%s://%s", protocol, hostname);
+ }
+
+ @JsonIgnore
+ public String getSshUrl() {
+ String protocol = isTlsEnabled() ? "https" : "http";
+ String hostname = getHost();
+ return String.format("%s://%s", protocol, hostname);
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/NfsWorkaroundConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/NfsWorkaroundConfig.java
new file mode 100644
index 0000000..ed4a4d1
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/NfsWorkaroundConfig.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+public class NfsWorkaroundConfig {
+
+ private boolean enabled = false;
+ private boolean chownOnStartup = false;
+ private String idmapdConfig;
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public boolean isChownOnStartup() {
+ return chownOnStartup;
+ }
+
+ public void setChownOnStartup(boolean chownOnStartup) {
+ this.chownOnStartup = chownOnStartup;
+ }
+
+ public String getIdmapdConfig() {
+ return idmapdConfig;
+ }
+
+ public void setIdmapdConfig(String idmapdConfig) {
+ this.idmapdConfig = idmapdConfig;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/SharedStorage.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/SharedStorage.java
new file mode 100644
index 0000000..d2193c1
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/SharedStorage.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+import io.fabric8.kubernetes.api.model.LabelSelector;
+import io.fabric8.kubernetes.api.model.Quantity;
+
+public class SharedStorage {
+ private ExternalPVCConfig externalPVC = new ExternalPVCConfig();
+ private Quantity size;
+ private String volumeName;
+ private LabelSelector selector;
+
+ public ExternalPVCConfig getExternalPVC() {
+ return externalPVC;
+ }
+
+ public void setExternalPVC(ExternalPVCConfig externalPVC) {
+ this.externalPVC = externalPVC;
+ }
+
+ public Quantity getSize() {
+ return size;
+ }
+
+ public String getVolumeName() {
+ return volumeName;
+ }
+
+ public void setSize(Quantity size) {
+ this.size = size;
+ }
+
+ public void setVolumeName(String volumeName) {
+ this.volumeName = volumeName;
+ }
+
+ public LabelSelector getSelector() {
+ return selector;
+ }
+
+ public void setSelector(LabelSelector selector) {
+ this.selector = selector;
+ }
+
+ public class ExternalPVCConfig {
+ private boolean enabled;
+ private String claimName = "";
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public String getClaimName() {
+ return claimName;
+ }
+
+ public void setClaimName(String claimName) {
+ this.claimName = claimName;
+ }
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/SpannerRefDbConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/SpannerRefDbConfig.java
new file mode 100644
index 0000000..eee7eab
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/SpannerRefDbConfig.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+public class SpannerRefDbConfig {
+ private String projectName;
+ private String instance;
+ private String database;
+
+ public String getProjectName() {
+ return projectName;
+ }
+
+ public void setProjectName(String projectName) {
+ this.projectName = projectName;
+ }
+
+ public String getInstance() {
+ return instance;
+ }
+
+ public void setInstance(String instance) {
+ this.instance = instance;
+ }
+
+ public String getDatabase() {
+ return database;
+ }
+
+ public void setDatabase(String database) {
+ this.database = database;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/StorageClassConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/StorageClassConfig.java
new file mode 100644
index 0000000..de4906b
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/StorageClassConfig.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+public class StorageClassConfig {
+
+ String readWriteOnce = "default";
+ String readWriteMany = "shared-storage";
+ NfsWorkaroundConfig nfsWorkaround = new NfsWorkaroundConfig();
+
+ public String getReadWriteOnce() {
+ return readWriteOnce;
+ }
+
+ public String getReadWriteMany() {
+ return readWriteMany;
+ }
+
+ public void setReadWriteOnce(String readWriteOnce) {
+ this.readWriteOnce = readWriteOnce;
+ }
+
+ public void setReadWriteMany(String readWriteMany) {
+ this.readWriteMany = readWriteMany;
+ }
+
+ public NfsWorkaroundConfig getNfsWorkaround() {
+ return nfsWorkaround;
+ }
+
+ public void setNfsWorkaround(NfsWorkaroundConfig nfsWorkaround) {
+ this.nfsWorkaround = nfsWorkaround;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/StorageConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/StorageConfig.java
new file mode 100644
index 0000000..566ce6e
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/StorageConfig.java
@@ -0,0 +1,44 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+public class StorageConfig {
+
+ private StorageClassConfig storageClasses;
+ private SharedStorage sharedStorage;
+
+ public StorageConfig() {}
+
+ public StorageConfig(GerritStorageConfig gerritStorageConfig) {
+ storageClasses = gerritStorageConfig.getStorageClasses();
+ sharedStorage = gerritStorageConfig.getSharedStorage();
+ }
+
+ public StorageClassConfig getStorageClasses() {
+ return storageClasses;
+ }
+
+ public void setStorageClasses(StorageClassConfig storageClasses) {
+ this.storageClasses = storageClasses;
+ }
+
+ public SharedStorage getSharedStorage() {
+ return sharedStorage;
+ }
+
+ public void setSharedStorage(SharedStorage sharedStorage) {
+ this.sharedStorage = sharedStorage;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/ZookeeperRefDbConfig.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/ZookeeperRefDbConfig.java
new file mode 100644
index 0000000..e90faeb
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/api/model/shared/ZookeeperRefDbConfig.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.api.model.shared;
+
+public class ZookeeperRefDbConfig {
+ private String connectString;
+ private String rootNode;
+
+ public String getConnectString() {
+ return connectString;
+ }
+
+ public void setConnectString(String connectString) {
+ this.connectString = connectString;
+ }
+
+ public String getRootNode() {
+ return rootNode;
+ }
+
+ public void setRootNode(String rootNode) {
+ this.rootNode = rootNode;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/gerrit/config/GerritConfigBuilder.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/gerrit/config/GerritConfigBuilder.java
new file mode 100644
index 0000000..221704d
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/gerrit/config/GerritConfigBuilder.java
@@ -0,0 +1,172 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.gerrit.config;
+
+import static com.google.gerrit.k8s.operator.gerrit.dependent.GerritStatefulSet.HTTP_PORT;
+import static com.google.gerrit.k8s.operator.gerrit.dependent.GerritStatefulSet.SSH_PORT;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.k8s.operator.gerrit.config.ConfigBuilder;
+import com.google.gerrit.k8s.operator.gerrit.config.RequiredOption;
+import com.google.gerrit.k8s.operator.gerrit.dependent.GerritService;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplateSpec.GerritMode;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.IngressConfig;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class GerritConfigBuilder extends ConfigBuilder {
+ private static final Pattern PROTOCOL_PATTERN = Pattern.compile("^(https?)://.+");
+
+ public GerritConfigBuilder(Gerrit gerrit) {
+ super(
+ gerrit.getSpec().getConfigFiles().getOrDefault("gerrit.config", ""),
+ ImmutableList.copyOf(collectRequiredOptions(gerrit)));
+ }
+
+ private static List<RequiredOption<?>> collectRequiredOptions(Gerrit gerrit) {
+ List<RequiredOption<?>> requiredOptions = new ArrayList<>();
+ requiredOptions.addAll(cacheSection(gerrit));
+ requiredOptions.addAll(containerSection(gerrit));
+ requiredOptions.addAll(gerritSection(gerrit));
+ requiredOptions.addAll(httpdSection(gerrit));
+ requiredOptions.addAll(sshdSection(gerrit));
+ return requiredOptions;
+ }
+
+ private static List<RequiredOption<?>> cacheSection(Gerrit gerrit) {
+ List<RequiredOption<?>> requiredOptions = new ArrayList<>();
+ requiredOptions.add(new RequiredOption<String>("cache", "directory", "cache"));
+ return requiredOptions;
+ }
+
+ private static List<RequiredOption<?>> containerSection(Gerrit gerrit) {
+ List<RequiredOption<?>> requiredOptions = new ArrayList<>();
+ requiredOptions.add(new RequiredOption<String>("container", "user", "gerrit"));
+ requiredOptions.add(
+ new RequiredOption<Boolean>(
+ "container", "replica", gerrit.getSpec().getMode().equals(GerritMode.REPLICA)));
+ requiredOptions.add(
+ new RequiredOption<String>("container", "javaHome", "/usr/lib/jvm/java-11-openjdk"));
+ requiredOptions.add(javaOptions(gerrit));
+ return requiredOptions;
+ }
+
+ private static List<RequiredOption<?>> gerritSection(Gerrit gerrit) {
+ List<RequiredOption<?>> requiredOptions = new ArrayList<>();
+ String serverId = gerrit.getSpec().getServerId();
+ requiredOptions.add(new RequiredOption<String>("gerrit", "basepath", "git"));
+ if (serverId != null && !serverId.isBlank()) {
+ requiredOptions.add(new RequiredOption<String>("gerrit", "serverId", serverId));
+ }
+
+ if (gerrit.getSpec().isHighlyAvailablePrimary()) {
+ requiredOptions.add(
+ new RequiredOption<Set<String>>(
+ "gerrit",
+ "installModule",
+ Set.of("com.gerritforge.gerrit.globalrefdb.validation.LibModule")));
+ requiredOptions.add(
+ new RequiredOption<Set<String>>(
+ "gerrit",
+ "installDbModule",
+ Set.of("com.ericsson.gerrit.plugins.highavailability.ValidationModule")));
+ }
+
+ IngressConfig ingressConfig = gerrit.getSpec().getIngress();
+ if (ingressConfig.isEnabled()) {
+ requiredOptions.add(
+ new RequiredOption<String>("gerrit", "canonicalWebUrl", ingressConfig.getUrl()));
+ }
+
+ return requiredOptions;
+ }
+
+ private static List<RequiredOption<?>> httpdSection(Gerrit gerrit) {
+ List<RequiredOption<?>> requiredOptions = new ArrayList<>();
+ IngressConfig ingressConfig = gerrit.getSpec().getIngress();
+ if (ingressConfig.isEnabled()) {
+ requiredOptions.add(listenUrl(ingressConfig.getUrl()));
+ }
+ return requiredOptions;
+ }
+
+ private static List<RequiredOption<?>> sshdSection(Gerrit gerrit) {
+ List<RequiredOption<?>> requiredOptions = new ArrayList<>();
+ requiredOptions.add(sshListenAddress(gerrit));
+ IngressConfig ingressConfig = gerrit.getSpec().getIngress();
+ if (ingressConfig.isEnabled() && gerrit.isSshEnabled()) {
+ requiredOptions.add(sshAdvertisedAddress(gerrit));
+ }
+ return requiredOptions;
+ }
+
+ private static RequiredOption<Set<String>> javaOptions(Gerrit gerrit) {
+ Set<String> javaOptions = new HashSet<>();
+ javaOptions.add("-Djavax.net.ssl.trustStore=/var/gerrit/etc/keystore");
+ if (gerrit.getSpec().isHighlyAvailablePrimary()) {
+ javaOptions.add("-Djava.net.preferIPv4Stack=true");
+ }
+ if (gerrit.getSpec().getDebug().isEnabled()) {
+ javaOptions.add("-Xdebug");
+ String debugServerCfg = "-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000";
+ if (gerrit.getSpec().getDebug().isSuspend()) {
+ debugServerCfg = debugServerCfg + ",suspend=y";
+ } else {
+ debugServerCfg = debugServerCfg + ",suspend=n";
+ }
+ javaOptions.add(debugServerCfg);
+ }
+ return new RequiredOption<Set<String>>("container", "javaOptions", javaOptions);
+ }
+
+ private static RequiredOption<String> listenUrl(String url) {
+ StringBuilder listenUrlBuilder = new StringBuilder();
+ listenUrlBuilder.append("proxy-");
+ Matcher protocolMatcher = PROTOCOL_PATTERN.matcher(url);
+ if (protocolMatcher.matches()) {
+ listenUrlBuilder.append(protocolMatcher.group(1));
+ } else {
+ throw new IllegalStateException(
+ String.format("Unknown protocol used for canonicalWebUrl: %s", url));
+ }
+ listenUrlBuilder.append("://*:");
+ listenUrlBuilder.append(HTTP_PORT);
+ listenUrlBuilder.append("/");
+ return new RequiredOption<String>("httpd", "listenUrl", listenUrlBuilder.toString());
+ }
+
+ private static RequiredOption<String> sshListenAddress(Gerrit gerrit) {
+ String listenAddress;
+ if (gerrit.isSshEnabled()) {
+ listenAddress = "*:" + SSH_PORT;
+ } else {
+ listenAddress = "off";
+ }
+ return new RequiredOption<String>("sshd", "listenAddress", listenAddress);
+ }
+
+ private static RequiredOption<String> sshAdvertisedAddress(Gerrit gerrit) {
+ return new RequiredOption<String>(
+ "sshd",
+ "advertisedAddress",
+ gerrit.getSpec().getIngress().getFullHostnameForService(GerritService.getName(gerrit))
+ + ":29418");
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/gerrit/config/HighAvailabilityPluginConfigBuilder.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/gerrit/config/HighAvailabilityPluginConfigBuilder.java
new file mode 100644
index 0000000..b96a93b
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/gerrit/config/HighAvailabilityPluginConfigBuilder.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.gerrit.config;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.k8s.operator.gerrit.config.ConfigBuilder;
+import com.google.gerrit.k8s.operator.gerrit.config.RequiredOption;
+import com.google.gerrit.k8s.operator.gerrit.dependent.GerritStatefulSet;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class HighAvailabilityPluginConfigBuilder extends ConfigBuilder {
+ public HighAvailabilityPluginConfigBuilder(Gerrit gerrit) {
+ super(
+ gerrit.getSpec().getConfigFiles().getOrDefault("high-availability.config", ""),
+ ImmutableList.copyOf(collectRequiredOptions(gerrit)));
+ }
+
+ private static List<RequiredOption<?>> collectRequiredOptions(Gerrit gerrit) {
+ List<RequiredOption<?>> requiredOptions = new ArrayList<>();
+ requiredOptions.add(new RequiredOption<String>("main", "sharedDirectory", "shared"));
+ requiredOptions.add(new RequiredOption<String>("peerInfo", "strategy", "jgroups"));
+ requiredOptions.add(new RequiredOption<String>("peerInfo", "jgroups", "myUrl", null));
+ requiredOptions.add(
+ new RequiredOption<String>("jgroups", "clusterName", gerrit.getMetadata().getName()));
+ requiredOptions.add(new RequiredOption<Boolean>("jgroups", "kubernetes", true));
+ requiredOptions.add(
+ new RequiredOption<String>(
+ "jgroups", "kubernetes", "namespace", gerrit.getMetadata().getNamespace()));
+ requiredOptions.add(
+ new RequiredOption<Set<String>>("jgroups", "kubernetes", "label", getLabels(gerrit)));
+ requiredOptions.add(new RequiredOption<Boolean>("cache", "synchronize", true));
+ requiredOptions.add(new RequiredOption<Boolean>("event", "synchronize", true));
+ requiredOptions.add(new RequiredOption<Boolean>("index", "synchronize", true));
+ requiredOptions.add(new RequiredOption<Boolean>("index", "synchronizeForced", true));
+ requiredOptions.add(new RequiredOption<Boolean>("healthcheck", "enable", true));
+ requiredOptions.add(new RequiredOption<Boolean>("ref-database", "enabled", true));
+ return requiredOptions;
+ }
+
+ private static Set<String> getLabels(Gerrit gerrit) {
+ Map<String, String> selectorLabels = GerritStatefulSet.getSelectorLabels(gerrit);
+ Set<String> labels = new HashSet<>();
+ for (Map.Entry<String, String> label : selectorLabels.entrySet()) {
+ labels.add(label.getKey() + "=" + label.getValue());
+ }
+ return labels;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/gerrit/config/SpannerRefDbPluginConfigBuilder.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/gerrit/config/SpannerRefDbPluginConfigBuilder.java
new file mode 100644
index 0000000..0f3cb30
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/gerrit/config/SpannerRefDbPluginConfigBuilder.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.gerrit.config;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.k8s.operator.gerrit.config.ConfigBuilder;
+import com.google.gerrit.k8s.operator.gerrit.config.RequiredOption;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import java.util.ArrayList;
+import java.util.List;
+
+public class SpannerRefDbPluginConfigBuilder extends ConfigBuilder {
+ public SpannerRefDbPluginConfigBuilder(Gerrit gerrit) {
+ super(
+ gerrit.getSpec().getConfigFiles().getOrDefault("spanner-refdb.config", ""),
+ ImmutableList.copyOf(collectRequiredOptions(gerrit)));
+ }
+
+ private static List<RequiredOption<?>> collectRequiredOptions(Gerrit gerrit) {
+ List<RequiredOption<?>> requiredOptions = new ArrayList<>();
+ requiredOptions.add(
+ new RequiredOption<String>("ref-database", "spanner", "useEmulator", "false"));
+ requiredOptions.add(
+ new RequiredOption<String>(
+ "ref-database",
+ "spanner",
+ "projectName",
+ gerrit.getSpec().getRefdb().getSpanner().getProjectName()));
+ requiredOptions.add(
+ new RequiredOption<String>(
+ "ref-database", "spanner", "credentialsPath", "/var/gerrit/etc/gcp-credentials.json"));
+ requiredOptions.add(
+ new RequiredOption<String>(
+ "ref-database",
+ "spanner",
+ "instance",
+ gerrit.getSpec().getRefdb().getSpanner().getInstance()));
+ requiredOptions.add(
+ new RequiredOption<String>(
+ "ref-database",
+ "spanner",
+ "database",
+ gerrit.getSpec().getRefdb().getSpanner().getDatabase()));
+ return requiredOptions;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/gerrit/config/ZookeeperRefDbPluginConfigBuilder.java b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/gerrit/config/ZookeeperRefDbPluginConfigBuilder.java
new file mode 100644
index 0000000..aabb726
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/java/com/google/gerrit/k8s/operator/v1alpha/gerrit/config/ZookeeperRefDbPluginConfigBuilder.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.v1alpha.gerrit.config;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.k8s.operator.gerrit.config.ConfigBuilder;
+import com.google.gerrit.k8s.operator.gerrit.config.RequiredOption;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ZookeeperRefDbPluginConfigBuilder extends ConfigBuilder {
+ public ZookeeperRefDbPluginConfigBuilder(Gerrit gerrit) {
+ super(
+ gerrit.getSpec().getConfigFiles().getOrDefault("zookeeper-refdb.config", ""),
+ ImmutableList.copyOf(collectRequiredOptions(gerrit)));
+ }
+
+ private static List<RequiredOption<?>> collectRequiredOptions(Gerrit gerrit) {
+ List<RequiredOption<?>> requiredOptions = new ArrayList<>();
+ requiredOptions.add(
+ new RequiredOption<String>(
+ "ref-database",
+ "zookeeper",
+ "connectString",
+ gerrit.getSpec().getRefdb().getZookeeper().getConnectString()));
+ requiredOptions.add(
+ new RequiredOption<String>(
+ "ref-database",
+ "zookeeper",
+ "rootNode",
+ gerrit.getSpec().getRefdb().getZookeeper().getRootNode()));
+ return requiredOptions;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/main/resources/META-INF/services/io.fabric8.kubernetes.api.model.KubernetesResource b/charts/k8s-gerrit/operator/src/main/resources/META-INF/services/io.fabric8.kubernetes.api.model.KubernetesResource
new file mode 100644
index 0000000..6fb15ac
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/resources/META-INF/services/io.fabric8.kubernetes.api.model.KubernetesResource
@@ -0,0 +1,5 @@
+com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster
+com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit
+com.google.gerrit.k8s.operator.v1alpha.api.model.gitgc.GitGarbageCollection
+com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.Receiver
+com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork
\ No newline at end of file
diff --git a/charts/k8s-gerrit/operator/src/main/resources/crd/emissary-crds.yaml b/charts/k8s-gerrit/operator/src/main/resources/crd/emissary-crds.yaml
new file mode 100644
index 0000000..bba936f
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/resources/crd/emissary-crds.yaml
@@ -0,0 +1,2589 @@
+# This file is downloaded from the Emissary repository on GitHub:
+# https://github.com/emissary-ingress/emissary/blob/master/manifests/emissary/emissary-crds.yaml.in
+#
+# Several modifications have been manually made:
+# 1. Only the `Mapping`, `TLSContext`, and `Host` CRDs have been kept from the source file. The source
+# file defines many CRDs that are not required by this operator project so the unnecessary CRDs have
+# been deleted.
+# 2. `v2ExplicitTLS` field has been removed from the Mapping CRD `v3alpha1` version. This is because
+# the "crd-to-java" generator plugin we use has a bug (https://github.com/fabric8io/kubernetes-client/issues/5457)
+# while converting enum types and the bug is triggered by the `v2ExplicitTLS` field. This field
+# may be added back in once we upgrade our fabric8 version to 6.8.x, where this bug is resolved.
+# 3. `ambassador_id` property is added to `Mapping`, `TLSContext`, and `Host` CRD version `v2`, by
+# copying it over from `v3`.
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.12.0
+ labels:
+ app.kubernetes.io/instance: emissary-apiext
+ app.kubernetes.io/managed-by: kubectl_apply_-f_emissary-apiext.yaml
+ app.kubernetes.io/name: emissary-apiext
+ app.kubernetes.io/part-of: emissary-apiext
+ name: mappings.getambassador.io
+spec:
+ conversion:
+ strategy: Webhook
+ webhook:
+ clientConfig:
+ service:
+ name: emissary-apiext
+ namespace: emissary-system
+ conversionReviewVersions:
+ - v1
+ group: getambassador.io
+ names:
+ categories:
+ - ambassador-crds
+ kind: Mapping
+ listKind: MappingList
+ plural: mappings
+ singular: mapping
+ preserveUnknownFields: false
+ scope: Namespaced
+ versions:
+ - additionalPrinterColumns:
+ - jsonPath: .spec.host
+ name: Source Host
+ type: string
+ - jsonPath: .spec.prefix
+ name: Source Prefix
+ type: string
+ - jsonPath: .spec.service
+ name: Dest Service
+ type: string
+ - jsonPath: .status.state
+ name: State
+ type: string
+ - jsonPath: .status.reason
+ name: Reason
+ type: string
+ name: v1
+ schema:
+ openAPIV3Schema:
+ description: Mapping is the Schema for the mappings API
+ properties:
+ apiVersion:
+ description: 'APIVersion defines the versioned schema of this representation
+ of an object. Servers should convert recognized schemas to the latest
+ internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+ type: string
+ kind:
+ description: 'Kind is a string value representing the REST resource this
+ object represents. Servers may infer this from the endpoint the client
+ submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+ type: string
+ metadata:
+ type: object
+ spec:
+ description: MappingSpec defines the desired state of Mapping
+ properties:
+ add_linkerd_headers:
+ type: boolean
+ add_request_headers:
+ type: object
+ x-kubernetes-preserve-unknown-fields: true
+ add_response_headers:
+ type: object
+ x-kubernetes-preserve-unknown-fields: true
+ allow_upgrade:
+ description: "A case-insensitive list of the non-HTTP protocols to
+ allow \"upgrading\" to from HTTP via the \"Connection: upgrade\"
+ mechanism[1]. After the upgrade, Ambassador does not interpret
+ the traffic, and behaves similarly to how it does for TCPMappings.
+ \n [1]: https://tools.ietf.org/html/rfc7230#section-6.7 \n For example,
+ if your upstream service supports WebSockets, you would write \n
+ allow_upgrade: - websocket \n Or if your upstream service supports
+ upgrading from HTTP to SPDY (as the Kubernetes apiserver does for
+ `kubectl exec` functionality), you would write \n allow_upgrade:
+ - spdy/3.1"
+ items:
+ type: string
+ type: array
+ auth_context_extensions:
+ additionalProperties:
+ type: string
+ type: object
+ auto_host_rewrite:
+ type: boolean
+ bypass_auth:
+ type: boolean
+ bypass_error_response_overrides:
+ description: If true, bypasses any `error_response_overrides` set
+ on the Ambassador module.
+ type: boolean
+ case_sensitive:
+ type: boolean
+ circuit_breakers:
+ items:
+ properties:
+ max_connections:
+ type: integer
+ max_pending_requests:
+ type: integer
+ max_requests:
+ type: integer
+ max_retries:
+ type: integer
+ priority:
+ enum:
+ - default
+ - high
+ type: string
+ type: object
+ type: array
+ cluster_idle_timeout_ms:
+ type: integer
+ cluster_max_connection_lifetime_ms:
+ type: integer
+ cluster_tag:
+ type: string
+ connect_timeout_ms:
+ type: integer
+ cors:
+ properties:
+ credentials:
+ type: boolean
+ max_age:
+ type: string
+ type: object
+ x-kubernetes-preserve-unknown-fields: true
+ dns_type:
+ type: string
+ docs:
+ description: DocsInfo provides some extra information about the docs
+ for the Mapping (used by the Dev Portal)
+ properties:
+ display_name:
+ type: string
+ ignored:
+ type: boolean
+ path:
+ type: string
+ timeout_ms:
+ type: integer
+ url:
+ type: string
+ type: object
+ enable_ipv4:
+ type: boolean
+ enable_ipv6:
+ type: boolean
+ envoy_override:
+ type: object
+ x-kubernetes-preserve-unknown-fields: true
+ error_response_overrides:
+ description: Error response overrides for this Mapping. Replaces all
+ of the `error_response_overrides` set on the Ambassador module,
+ if any.
+ items:
+ description: A response rewrite for an HTTP error response
+ properties:
+ body:
+ description: The new response body
+ properties:
+ content_type:
+ description: The content type to set on the error response
+ body when using text_format or text_format_source. Defaults
+ to 'text/plain'.
+ type: string
+ json_format:
+ additionalProperties:
+ type: string
+ description: 'A JSON response with content-type: application/json.
+ The values can contain format text like in text_format.'
+ type: object
+ text_format:
+ description: A format string representing a text response
+ body. Content-Type can be set using the `content_type`
+ field below.
+ type: string
+ text_format_source:
+ description: A format string sourced from a file on the
+ Ambassador container. Useful for larger response bodies
+ that should not be placed inline in configuration.
+ properties:
+ filename:
+ description: The name of a file on the Ambassador pod
+ that contains a format text string.
+ type: string
+ type: object
+ type: object
+ on_status_code:
+ description: The status code to match on -- not a pointer because
+ it's required.
+ maximum: 599
+ minimum: 400
+ type: integer
+ required:
+ - body
+ - on_status_code
+ type: object
+ minItems: 1
+ type: array
+ grpc:
+ type: boolean
+ headers:
+ type: object
+ x-kubernetes-preserve-unknown-fields: true
+ host:
+ type: string
+ host_redirect:
+ type: boolean
+ host_regex:
+ type: boolean
+ host_rewrite:
+ type: string
+ idle_timeout_ms:
+ type: integer
+ keepalive:
+ properties:
+ idle_time:
+ type: integer
+ interval:
+ type: integer
+ probes:
+ type: integer
+ type: object
+ labels:
+ additionalProperties:
+ description: A MappingLabelGroupsArray is an array of MappingLabelGroups.
+ I know, complex.
+ items:
+ description: 'A MappingLabelGroup is a single element of a MappingLabelGroupsArray:
+ a second map, where the key is a human-readable name that identifies
+ the group.'
+ maxProperties: 1
+ minProperties: 1
+ type: object
+ x-kubernetes-preserve-unknown-fields: true
+ type: array
+ description: A DomainMap is the overall Mapping.spec.Labels type.
+ It maps domains (kind of like namespaces for Mapping labels) to
+ arrays of label groups.
+ type: object
+ load_balancer:
+ properties:
+ cookie:
+ properties:
+ name:
+ type: string
+ path:
+ type: string
+ ttl:
+ type: string
+ required:
+ - name
+ type: object
+ header:
+ type: string
+ policy:
+ enum:
+ - round_robin
+ - ring_hash
+ - maglev
+ - least_request
+ type: string
+ source_ip:
+ type: boolean
+ required:
+ - policy
+ type: object
+ method:
+ type: string
+ method_regex:
+ type: boolean
+ modules:
+ items:
+ type: object
+ x-kubernetes-preserve-unknown-fields: true
+ type: array
+ outlier_detection:
+ type: string
+ path_redirect:
+ description: Path replacement to use when generating an HTTP redirect.
+ Used with `host_redirect`.
+ type: string
+ precedence:
+ type: integer
+ prefix:
+ type: string
+ prefix_exact:
+ type: boolean
+ prefix_redirect:
+ description: Prefix rewrite to use when generating an HTTP redirect.
+ Used with `host_redirect`.
+ type: string
+ prefix_regex:
+ type: boolean
+ priority:
+ type: string
+ query_parameters:
+ type: object
+ x-kubernetes-preserve-unknown-fields: true
+ redirect_response_code:
+ description: The response code to use when generating an HTTP redirect.
+ Defaults to 301. Used with `host_redirect`.
+ enum:
+ - 301
+ - 302
+ - 303
+ - 307
+ - 308
+ type: integer
+ regex_headers:
+ additionalProperties:
+ type: string
+ type: object
+ regex_query_parameters:
+ additionalProperties:
+ type: string
+ type: object
+ regex_redirect:
+ description: Prefix regex rewrite to use when generating an HTTP redirect.
+ Used with `host_redirect`.
+ properties:
+ pattern:
+ type: string
+ substitution:
+ type: string
+ type: object
+ regex_rewrite:
+ properties:
+ pattern:
+ type: string
+ substitution:
+ type: string
+ type: object
+ resolver:
+ type: string
+ respect_dns_ttl:
+ type: boolean
+ retry_policy:
+ properties:
+ num_retries:
+ type: integer
+ per_try_timeout:
+ type: string
+ retry_on:
+ enum:
+ - 5xx
+ - gateway-error
+ - connect-failure
+ - retriable-4xx
+ - refused-stream
+ - retriable-status-codes
+ type: string
+ type: object
+ rewrite:
+ type: string
+ service:
+ type: string
+ shadow:
+ type: boolean
+ timeout_ms:
+ description: The timeout for requests that use this Mapping. Overrides
+ `cluster_request_timeout_ms` set on the Ambassador Module, if it
+ exists.
+ type: integer
+ use_websocket:
+ description: 'use_websocket is deprecated, and is equivlaent to setting
+ `allow_upgrade: ["websocket"]`'
+ type: boolean
+ v3StatsName:
+ type: string
+ v3health_checks:
+ items:
+ description: HealthCheck specifies settings for performing active
+ health checking on upstreams
+ properties:
+ health_check:
+ description: Configuration for where the healthcheck request
+ should be made to
+ maxProperties: 1
+ minProperties: 1
+ properties:
+ grpc:
+ description: HealthCheck for gRPC upstreams. Only one of
+ grpc_health_check or http_health_check may be specified
+ properties:
+ authority:
+ description: The value of the :authority header in the
+ gRPC health check request. If left empty the upstream
+ name will be used.
+ type: string
+ upstream_name:
+ description: The upstream name parameter which will
+ be sent to gRPC service in the health check message
+ type: string
+ required:
+ - upstream_name
+ type: object
+ http:
+ description: HealthCheck for HTTP upstreams. Only one of
+ http_health_check or grpc_health_check may be specified
+ properties:
+ add_request_headers:
+ additionalProperties:
+ properties:
+ append:
+ type: boolean
+ v2Representation:
+ enum:
+ - ""
+ - string
+ - "null"
+ type: string
+ value:
+ type: string
+ type: object
+ type: object
+ expected_statuses:
+ items:
+ description: A range of response statuses from Start
+ to End inclusive
+ properties:
+ max:
+ description: End of the statuses to include. Must
+ be between 100 and 599 (inclusive)
+ maximum: 599
+ minimum: 100
+ type: integer
+ min:
+ description: Start of the statuses to include.
+ Must be between 100 and 599 (inclusive)
+ maximum: 599
+ minimum: 100
+ type: integer
+ required:
+ - max
+ - min
+ type: object
+ type: array
+ hostname:
+ type: string
+ path:
+ type: string
+ remove_request_headers:
+ items:
+ type: string
+ type: array
+ required:
+ - path
+ type: object
+ type: object
+ healthy_threshold:
+ description: Number of expected responses for the upstream to
+ be considered healthy. Defaults to 1.
+ type: integer
+ interval:
+ description: Interval between health checks. Defaults to every
+ 5 seconds.
+ type: string
+ timeout:
+ description: Timeout for connecting to the health checking endpoint.
+ Defaults to 3 seconds.
+ type: string
+ unhealthy_threshold:
+ description: Number of non-expected responses for the upstream
+ to be considered unhealthy. A single 503 will mark the upstream
+ as unhealthy regardless of the threshold. Defaults to 2.
+ type: integer
+ required:
+ - health_check
+ type: object
+ minItems: 1
+ type: array
+ weight:
+ type: integer
+ required:
+ - prefix
+ - service
+ type: object
+ x-kubernetes-preserve-unknown-fields: true
+ status:
+ description: MappingStatus defines the observed state of Mapping
+ properties:
+ reason:
+ type: string
+ state:
+ enum:
+ - ""
+ - Inactive
+ - Running
+ type: string
+ type: object
+ type: object
+ served: true
+ storage: false
+ subresources:
+ status: {}
+ - additionalPrinterColumns:
+ - jsonPath: .spec.host
+ name: Source Host
+ type: string
+ - jsonPath: .spec.prefix
+ name: Source Prefix
+ type: string
+ - jsonPath: .spec.service
+ name: Dest Service
+ type: string
+ - jsonPath: .status.state
+ name: State
+ type: string
+ - jsonPath: .status.reason
+ name: Reason
+ type: string
+ name: v2
+ schema:
+ openAPIV3Schema:
+ description: Mapping is the Schema for the mappings API
+ properties:
+ apiVersion:
+ description: 'APIVersion defines the versioned schema of this representation
+ of an object. Servers should convert recognized schemas to the latest
+ internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+ type: string
+ kind:
+ description: 'Kind is a string value representing the REST resource this
+ object represents. Servers may infer this from the endpoint the client
+ submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+ type: string
+ metadata:
+ type: object
+ spec:
+ description: MappingSpec defines the desired state of Mapping
+ properties:
+ add_linkerd_headers:
+ type: boolean
+ add_request_headers:
+ type: object
+ x-kubernetes-preserve-unknown-fields: true
+ add_response_headers:
+ type: object
+ x-kubernetes-preserve-unknown-fields: true
+ allow_upgrade:
+ description: "A case-insensitive list of the non-HTTP protocols to
+ allow \"upgrading\" to from HTTP via the \"Connection: upgrade\"
+ mechanism[1]. After the upgrade, Ambassador does not interpret
+ the traffic, and behaves similarly to how it does for TCPMappings.
+ \n [1]: https://tools.ietf.org/html/rfc7230#section-6.7 \n For example,
+ if your upstream service supports WebSockets, you would write \n
+ allow_upgrade: - websocket \n Or if your upstream service supports
+ upgrading from HTTP to SPDY (as the Kubernetes apiserver does for
+ `kubectl exec` functionality), you would write \n allow_upgrade:
+ - spdy/3.1"
+ items:
+ type: string
+ type: array
+ # [operator] added manually by coping over from v3alpha1
+ ambassador_id:
+ description: "AmbassadorID declares which Ambassador instances should
+ pay attention to this resource. If no value is provided, the default
+ is: \n ambassador_id: - \"default\""
+ items:
+ type: string
+ type: array
+ auth_context_extensions:
+ additionalProperties:
+ type: string
+ type: object
+ auto_host_rewrite:
+ type: boolean
+ bypass_auth:
+ type: boolean
+ bypass_error_response_overrides:
+ description: If true, bypasses any `error_response_overrides` set
+ on the Ambassador module.
+ type: boolean
+ case_sensitive:
+ type: boolean
+ circuit_breakers:
+ items:
+ properties:
+ max_connections:
+ type: integer
+ max_pending_requests:
+ type: integer
+ max_requests:
+ type: integer
+ max_retries:
+ type: integer
+ priority:
+ enum:
+ - default
+ - high
+ type: string
+ type: object
+ type: array
+ cluster_idle_timeout_ms:
+ type: integer
+ cluster_max_connection_lifetime_ms:
+ type: integer
+ cluster_tag:
+ type: string
+ connect_timeout_ms:
+ type: integer
+ cors:
+ properties:
+ credentials:
+ type: boolean
+ max_age:
+ type: string
+ type: object
+ x-kubernetes-preserve-unknown-fields: true
+ dns_type:
+ type: string
+ docs:
+ description: DocsInfo provides some extra information about the docs
+ for the Mapping (used by the Dev Portal)
+ properties:
+ display_name:
+ type: string
+ ignored:
+ type: boolean
+ path:
+ type: string
+ timeout_ms:
+ type: integer
+ url:
+ type: string
+ type: object
+ enable_ipv4:
+ type: boolean
+ enable_ipv6:
+ type: boolean
+ envoy_override:
+ type: object
+ x-kubernetes-preserve-unknown-fields: true
+ error_response_overrides:
+ description: Error response overrides for this Mapping. Replaces all
+ of the `error_response_overrides` set on the Ambassador module,
+ if any.
+ items:
+ description: A response rewrite for an HTTP error response
+ properties:
+ body:
+ description: The new response body
+ properties:
+ content_type:
+ description: The content type to set on the error response
+ body when using text_format or text_format_source. Defaults
+ to 'text/plain'.
+ type: string
+ json_format:
+ additionalProperties:
+ type: string
+ description: 'A JSON response with content-type: application/json.
+ The values can contain format text like in text_format.'
+ type: object
+ text_format:
+ description: A format string representing a text response
+ body. Content-Type can be set using the `content_type`
+ field below.
+ type: string
+ text_format_source:
+ description: A format string sourced from a file on the
+ Ambassador container. Useful for larger response bodies
+ that should not be placed inline in configuration.
+ properties:
+ filename:
+ description: The name of a file on the Ambassador pod
+ that contains a format text string.
+ type: string
+ type: object
+ type: object
+ on_status_code:
+ description: The status code to match on -- not a pointer because
+ it's required.
+ maximum: 599
+ minimum: 400
+ type: integer
+ required:
+ - body
+ - on_status_code
+ type: object
+ minItems: 1
+ type: array
+ grpc:
+ type: boolean
+ headers:
+ type: object
+ x-kubernetes-preserve-unknown-fields: true
+ host:
+ type: string
+ host_redirect:
+ type: boolean
+ host_regex:
+ type: boolean
+ host_rewrite:
+ type: string
+ idle_timeout_ms:
+ type: integer
+ keepalive:
+ properties:
+ idle_time:
+ type: integer
+ interval:
+ type: integer
+ probes:
+ type: integer
+ type: object
+ labels:
+ additionalProperties:
+ description: A MappingLabelGroupsArray is an array of MappingLabelGroups.
+ I know, complex.
+ items:
+ description: 'A MappingLabelGroup is a single element of a MappingLabelGroupsArray:
+ a second map, where the key is a human-readable name that identifies
+ the group.'
+ maxProperties: 1
+ minProperties: 1
+ type: object
+ x-kubernetes-preserve-unknown-fields: true
+ type: array
+ description: A DomainMap is the overall Mapping.spec.Labels type.
+ It maps domains (kind of like namespaces for Mapping labels) to
+ arrays of label groups.
+ type: object
+ load_balancer:
+ properties:
+ cookie:
+ properties:
+ name:
+ type: string
+ path:
+ type: string
+ ttl:
+ type: string
+ required:
+ - name
+ type: object
+ header:
+ type: string
+ policy:
+ enum:
+ - round_robin
+ - ring_hash
+ - maglev
+ - least_request
+ type: string
+ source_ip:
+ type: boolean
+ required:
+ - policy
+ type: object
+ method:
+ type: string
+ method_regex:
+ type: boolean
+ modules:
+ items:
+ type: object
+ x-kubernetes-preserve-unknown-fields: true
+ type: array
+ outlier_detection:
+ type: string
+ path_redirect:
+ description: Path replacement to use when generating an HTTP redirect.
+ Used with `host_redirect`.
+ type: string
+ precedence:
+ type: integer
+ prefix:
+ type: string
+ prefix_exact:
+ type: boolean
+ prefix_redirect:
+ description: Prefix rewrite to use when generating an HTTP redirect.
+ Used with `host_redirect`.
+ type: string
+ prefix_regex:
+ type: boolean
+ priority:
+ type: string
+ query_parameters:
+ type: object
+ x-kubernetes-preserve-unknown-fields: true
+ redirect_response_code:
+ description: The response code to use when generating an HTTP redirect.
+ Defaults to 301. Used with `host_redirect`.
+ enum:
+ - 301
+ - 302
+ - 303
+ - 307
+ - 308
+ type: integer
+ regex_headers:
+ additionalProperties:
+ type: string
+ type: object
+ regex_query_parameters:
+ additionalProperties:
+ type: string
+ type: object
+ regex_redirect:
+ description: Prefix regex rewrite to use when generating an HTTP redirect.
+ Used with `host_redirect`.
+ properties:
+ pattern:
+ type: string
+ substitution:
+ type: string
+ type: object
+ regex_rewrite:
+ properties:
+ pattern:
+ type: string
+ substitution:
+ type: string
+ type: object
+ resolver:
+ type: string
+ respect_dns_ttl:
+ type: boolean
+ retry_policy:
+ properties:
+ num_retries:
+ type: integer
+ per_try_timeout:
+ type: string
+ retry_on:
+ enum:
+ - 5xx
+ - gateway-error
+ - connect-failure
+ - retriable-4xx
+ - refused-stream
+ - retriable-status-codes
+ type: string
+ type: object
+ rewrite:
+ type: string
+ service:
+ type: string
+ shadow:
+ type: boolean
+ timeout_ms:
+ description: The timeout for requests that use this Mapping. Overrides
+ `cluster_request_timeout_ms` set on the Ambassador Module, if it
+ exists.
+ type: integer
+ use_websocket:
+ description: 'use_websocket is deprecated, and is equivlaent to setting
+ `allow_upgrade: ["websocket"]`'
+ type: boolean
+ v3StatsName:
+ type: string
+ v3health_checks:
+ items:
+ description: HealthCheck specifies settings for performing active
+ health checking on upstreams
+ properties:
+ health_check:
+ description: Configuration for where the healthcheck request
+ should be made to
+ maxProperties: 1
+ minProperties: 1
+ properties:
+ grpc:
+ description: HealthCheck for gRPC upstreams. Only one of
+ grpc_health_check or http_health_check may be specified
+ properties:
+ authority:
+ description: The value of the :authority header in the
+ gRPC health check request. If left empty the upstream
+ name will be used.
+ type: string
+ upstream_name:
+ description: The upstream name parameter which will
+ be sent to gRPC service in the health check message
+ type: string
+ required:
+ - upstream_name
+ type: object
+ http:
+ description: HealthCheck for HTTP upstreams. Only one of
+ http_health_check or grpc_health_check may be specified
+ properties:
+ add_request_headers:
+ additionalProperties:
+ properties:
+ append:
+ type: boolean
+ v2Representation:
+ enum:
+ - ""
+ - string
+ - "null"
+ type: string
+ value:
+ type: string
+ type: object
+ type: object
+ expected_statuses:
+ items:
+ description: A range of response statuses from Start
+ to End inclusive
+ properties:
+ max:
+ description: End of the statuses to include. Must
+ be between 100 and 599 (inclusive)
+ maximum: 599
+ minimum: 100
+ type: integer
+ min:
+ description: Start of the statuses to include.
+ Must be between 100 and 599 (inclusive)
+ maximum: 599
+ minimum: 100
+ type: integer
+ required:
+ - max
+ - min
+ type: object
+ type: array
+ hostname:
+ type: string
+ path:
+ type: string
+ remove_request_headers:
+ items:
+ type: string
+ type: array
+ required:
+ - path
+ type: object
+ type: object
+ healthy_threshold:
+ description: Number of expected responses for the upstream to
+ be considered healthy. Defaults to 1.
+ type: integer
+ interval:
+ description: Interval between health checks. Defaults to every
+ 5 seconds.
+ type: string
+ timeout:
+ description: Timeout for connecting to the health checking endpoint.
+ Defaults to 3 seconds.
+ type: string
+ unhealthy_threshold:
+ description: Number of non-expected responses for the upstream
+ to be considered unhealthy. A single 503 will mark the upstream
+ as unhealthy regardless of the threshold. Defaults to 2.
+ type: integer
+ required:
+ - health_check
+ type: object
+ minItems: 1
+ type: array
+ weight:
+ type: integer
+ required:
+ - prefix
+ - service
+ type: object
+ x-kubernetes-preserve-unknown-fields: true
+ status:
+ description: MappingStatus defines the observed state of Mapping
+ properties:
+ reason:
+ type: string
+ state:
+ enum:
+ - ""
+ - Inactive
+ - Running
+ type: string
+ type: object
+ type: object
+ served: true
+ storage: true
+ subresources:
+ status: {}
+ - additionalPrinterColumns:
+ - jsonPath: .spec.host
+ name: Source Host
+ type: string
+ - jsonPath: .spec.prefix
+ name: Source Prefix
+ type: string
+ - jsonPath: .spec.service
+ name: Dest Service
+ type: string
+ - jsonPath: .status.state
+ name: State
+ type: string
+ - jsonPath: .status.reason
+ name: Reason
+ type: string
+ name: v3alpha1
+ schema:
+ openAPIV3Schema:
+ description: Mapping is the Schema for the mappings API
+ properties:
+ apiVersion:
+ description: 'APIVersion defines the versioned schema of this representation
+ of an object. Servers should convert recognized schemas to the latest
+ internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+ type: string
+ kind:
+ description: 'Kind is a string value representing the REST resource this
+ object represents. Servers may infer this from the endpoint the client
+ submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+ type: string
+ metadata:
+ type: object
+ spec:
+ description: MappingSpec defines the desired state of Mapping
+ properties:
+ add_linkerd_headers:
+ type: boolean
+ add_request_headers:
+ additionalProperties:
+ properties:
+ append:
+ type: boolean
+ v2Representation:
+ enum:
+ - ""
+ - string
+ - "null"
+ type: string
+ value:
+ type: string
+ type: object
+ type: object
+ add_response_headers:
+ additionalProperties:
+ properties:
+ append:
+ type: boolean
+ v2Representation:
+ enum:
+ - ""
+ - string
+ - "null"
+ type: string
+ value:
+ type: string
+ type: object
+ type: object
+ allow_upgrade:
+ description: "A case-insensitive list of the non-HTTP protocols to
+ allow \"upgrading\" to from HTTP via the \"Connection: upgrade\"
+ mechanism[1]. After the upgrade, Ambassador does not interpret
+ the traffic, and behaves similarly to how it does for TCPMappings.
+ \n [1]: https://tools.ietf.org/html/rfc7230#section-6.7 \n For example,
+ if your upstream service supports WebSockets, you would write \n
+ allow_upgrade: - websocket \n Or if your upstream service supports
+ upgrading from HTTP to SPDY (as the Kubernetes apiserver does for
+ `kubectl exec` functionality), you would write \n allow_upgrade:
+ - spdy/3.1"
+ items:
+ type: string
+ type: array
+ ambassador_id:
+ description: "AmbassadorID declares which Ambassador instances should
+ pay attention to this resource. If no value is provided, the default
+ is: \n ambassador_id: - \"default\""
+ items:
+ type: string
+ type: array
+ auth_context_extensions:
+ additionalProperties:
+ type: string
+ type: object
+ auto_host_rewrite:
+ type: boolean
+ bypass_auth:
+ type: boolean
+ bypass_error_response_overrides:
+ description: If true, bypasses any `error_response_overrides` set
+ on the Ambassador module.
+ type: boolean
+ case_sensitive:
+ type: boolean
+ circuit_breakers:
+ items:
+ properties:
+ max_connections:
+ type: integer
+ max_pending_requests:
+ type: integer
+ max_requests:
+ type: integer
+ max_retries:
+ type: integer
+ priority:
+ enum:
+ - default
+ - high
+ type: string
+ type: object
+ type: array
+ cluster_idle_timeout_ms:
+ type: integer
+ cluster_max_connection_lifetime_ms:
+ type: integer
+ cluster_tag:
+ type: string
+ connect_timeout_ms:
+ type: integer
+ cors:
+ properties:
+ credentials:
+ type: boolean
+ exposed_headers:
+ items:
+ type: string
+ type: array
+ headers:
+ items:
+ type: string
+ type: array
+ max_age:
+ type: string
+ methods:
+ items:
+ type: string
+ type: array
+ origins:
+ items:
+ type: string
+ type: array
+ v2CommaSeparatedOrigins:
+ type: boolean
+ type: object
+ dns_type:
+ type: string
+ docs:
+ description: DocsInfo provides some extra information about the docs
+ for the Mapping. Docs is used by both the agent and the DevPortal.
+ properties:
+ display_name:
+ type: string
+ ignored:
+ type: boolean
+ path:
+ type: string
+ timeout_ms:
+ type: integer
+ url:
+ type: string
+ type: object
+ enable_ipv4:
+ type: boolean
+ enable_ipv6:
+ type: boolean
+ envoy_override:
+ type: object
+ x-kubernetes-preserve-unknown-fields: true
+ error_response_overrides:
+ description: Error response overrides for this Mapping. Replaces all
+ of the `error_response_overrides` set on the Ambassador module,
+ if any.
+ items:
+ description: A response rewrite for an HTTP error response
+ properties:
+ body:
+ description: The new response body
+ properties:
+ content_type:
+ description: The content type to set on the error response
+ body when using text_format or text_format_source. Defaults
+ to 'text/plain'.
+ type: string
+ json_format:
+ additionalProperties:
+ type: string
+ description: 'A JSON response with content-type: application/json.
+ The values can contain format text like in text_format.'
+ type: object
+ text_format:
+ description: A format string representing a text response
+ body. Content-Type can be set using the `content_type`
+ field below.
+ type: string
+ text_format_source:
+ description: A format string sourced from a file on the
+ Ambassador container. Useful for larger response bodies
+ that should not be placed inline in configuration.
+ properties:
+ filename:
+ description: The name of a file on the Ambassador pod
+ that contains a format text string.
+ type: string
+ type: object
+ type: object
+ on_status_code:
+ description: The status code to match on -- not a pointer because
+ it's required.
+ maximum: 599
+ minimum: 400
+ type: integer
+ required:
+ - body
+ - on_status_code
+ type: object
+ minItems: 1
+ type: array
+ grpc:
+ type: boolean
+ headers:
+ additionalProperties:
+ type: string
+ type: object
+ health_checks:
+ items:
+ description: HealthCheck specifies settings for performing active
+ health checking on upstreams
+ properties:
+ health_check:
+ description: Configuration for where the healthcheck request
+ should be made to
+ maxProperties: 1
+ minProperties: 1
+ properties:
+ grpc:
+ description: HealthCheck for gRPC upstreams. Only one of
+ grpc_health_check or http_health_check may be specified
+ properties:
+ authority:
+ description: The value of the :authority header in the
+ gRPC health check request. If left empty the upstream
+ name will be used.
+ type: string
+ upstream_name:
+ description: The upstream name parameter which will
+ be sent to gRPC service in the health check message
+ type: string
+ required:
+ - upstream_name
+ type: object
+ http:
+ description: HealthCheck for HTTP upstreams. Only one of
+ http_health_check or grpc_health_check may be specified
+ properties:
+ add_request_headers:
+ additionalProperties:
+ properties:
+ append:
+ type: boolean
+ v2Representation:
+ enum:
+ - ""
+ - string
+ - "null"
+ type: string
+ value:
+ type: string
+ type: object
+ type: object
+ expected_statuses:
+ items:
+ description: A range of response statuses from Start
+ to End inclusive
+ properties:
+ max:
+ description: End of the statuses to include. Must
+ be between 100 and 599 (inclusive)
+ maximum: 599
+ minimum: 100
+ type: integer
+ min:
+ description: Start of the statuses to include.
+ Must be between 100 and 599 (inclusive)
+ maximum: 599
+ minimum: 100
+ type: integer
+ required:
+ - max
+ - min
+ type: object
+ type: array
+ hostname:
+ type: string
+ path:
+ type: string
+ remove_request_headers:
+ items:
+ type: string
+ type: array
+ required:
+ - path
+ type: object
+ type: object
+ healthy_threshold:
+ description: Number of expected responses for the upstream to
+ be considered healthy. Defaults to 1.
+ type: integer
+ interval:
+ description: Interval between health checks. Defaults to every
+ 5 seconds.
+ type: string
+ timeout:
+ description: Timeout for connecting to the health checking endpoint.
+ Defaults to 3 seconds.
+ type: string
+ unhealthy_threshold:
+ description: Number of non-expected responses for the upstream
+ to be considered unhealthy. A single 503 will mark the upstream
+ as unhealthy regardless of the threshold. Defaults to 2.
+ type: integer
+ required:
+ - health_check
+ type: object
+ minItems: 1
+ type: array
+ host:
+ description: "Exact match for the hostname of a request if HostRegex
+ is false; regex match for the hostname if HostRegex is true. \n
+ Host specifies both a match for the ':authority' header of a request,
+ as well as a match criterion for Host CRDs: a Mapping that specifies
+ Host will not associate with a Host that doesn't have a matching
+ Hostname. \n If both Host and Hostname are set, an error is logged,
+ Host is ignored, and Hostname is used. \n DEPRECATED: Host is either
+ an exact match or a regex, depending on HostRegex. Use HostName
+ instead."
+ type: string
+ host_redirect:
+ type: boolean
+ host_regex:
+ description: 'DEPRECATED: Host is either an exact match or a regex,
+ depending on HostRegex. Use HostName instead.'
+ type: boolean
+ host_rewrite:
+ type: string
+ hostname:
+ description: "Hostname is a DNS glob specifying the hosts to which
+ this Mapping applies. \n Hostname specifies both a match for the
+ ':authority' header of a request, as well as a match criterion for
+ Host CRDs: a Mapping that specifies Hostname will not associate
+ with a Host that doesn't have a matching Hostname. \n If both Host
+ and Hostname are set, an error is logged, Host is ignored, and Hostname
+ is used."
+ type: string
+ idle_timeout_ms:
+ type: integer
+ keepalive:
+ properties:
+ idle_time:
+ type: integer
+ interval:
+ type: integer
+ probes:
+ type: integer
+ type: object
+ labels:
+ additionalProperties:
+ description: A MappingLabelGroupsArray is an array of MappingLabelGroups.
+ I know, complex.
+ items:
+ additionalProperties:
+ description: 'A MappingLabelsArray is the value in the MappingLabelGroup:
+ an array of label specifiers.'
+ items:
+ description: "A MappingLabelSpecifier (finally!) defines a
+ single label. \n This mimics envoy/config/route/v3/route_components.proto:RateLimit:Action:action_specifier."
+ maxProperties: 1
+ minProperties: 1
+ properties:
+ destination_cluster:
+ description: Sets the label "destination_cluster=«Envoy
+ destination cluster name»".
+ properties:
+ key:
+ enum:
+ - destination_cluster
+ type: string
+ required:
+ - key
+ type: object
+ generic_key:
+ description: Sets the label "«key»=«value»" (where by
+ default «key» is "generic_key").
+ properties:
+ key:
+ description: The default is "generic_key".
+ type: string
+ v2Shorthand:
+ type: boolean
+ value:
+ type: string
+ required:
+ - value
+ type: object
+ remote_address:
+ description: Sets the label "remote_address=«IP address
+ of the client»".
+ properties:
+ key:
+ enum:
+ - remote_address
+ type: string
+ required:
+ - key
+ type: object
+ request_headers:
+ description: If the «header_name» header is set, then
+ set the label "«key»=«Value of the «header_name» header»";
+ otherwise skip applying this label group.
+ properties:
+ header_name:
+ type: string
+ key:
+ type: string
+ omit_if_not_present:
+ type: boolean
+ required:
+ - header_name
+ - key
+ type: object
+ source_cluster:
+ description: Sets the label "source_cluster=«Envoy source
+ cluster name»".
+ properties:
+ key:
+ enum:
+ - source_cluster
+ type: string
+ required:
+ - key
+ type: object
+ type: object
+ type: array
+ description: 'A MappingLabelGroup is a single element of a MappingLabelGroupsArray:
+ a second map, where the key is a human-readable name that identifies
+ the group.'
+ maxProperties: 1
+ minProperties: 1
+ type: object
+ type: array
+ description: A DomainMap is the overall Mapping.spec.Labels type.
+ It maps domains (kind of like namespaces for Mapping labels) to
+ arrays of label groups.
+ type: object
+ load_balancer:
+ properties:
+ cookie:
+ properties:
+ name:
+ type: string
+ path:
+ type: string
+ ttl:
+ type: string
+ required:
+ - name
+ type: object
+ header:
+ type: string
+ policy:
+ enum:
+ - round_robin
+ - ring_hash
+ - maglev
+ - least_request
+ type: string
+ source_ip:
+ type: boolean
+ required:
+ - policy
+ type: object
+ method:
+ type: string
+ method_regex:
+ type: boolean
+ modules:
+ items:
+ type: object
+ x-kubernetes-preserve-unknown-fields: true
+ type: array
+ outlier_detection:
+ type: string
+ path_redirect:
+ description: Path replacement to use when generating an HTTP redirect.
+ Used with `host_redirect`.
+ type: string
+ precedence:
+ type: integer
+ prefix:
+ type: string
+ prefix_exact:
+ type: boolean
+ prefix_redirect:
+ description: Prefix rewrite to use when generating an HTTP redirect.
+ Used with `host_redirect`.
+ type: string
+ prefix_regex:
+ type: boolean
+ priority:
+ type: string
+ query_parameters:
+ additionalProperties:
+ type: string
+ type: object
+ redirect_response_code:
+ description: The response code to use when generating an HTTP redirect.
+ Defaults to 301. Used with `host_redirect`.
+ enum:
+ - 301
+ - 302
+ - 303
+ - 307
+ - 308
+ type: integer
+ regex_headers:
+ additionalProperties:
+ type: string
+ type: object
+ regex_query_parameters:
+ additionalProperties:
+ type: string
+ type: object
+ regex_redirect:
+ description: Prefix regex rewrite to use when generating an HTTP redirect.
+ Used with `host_redirect`.
+ properties:
+ pattern:
+ type: string
+ substitution:
+ type: string
+ type: object
+ regex_rewrite:
+ properties:
+ pattern:
+ type: string
+ substitution:
+ type: string
+ type: object
+ remove_request_headers:
+ items:
+ type: string
+ type: array
+ remove_response_headers:
+ items:
+ type: string
+ type: array
+ resolver:
+ type: string
+ respect_dns_ttl:
+ type: boolean
+ retry_policy:
+ properties:
+ num_retries:
+ type: integer
+ per_try_timeout:
+ type: string
+ retry_on:
+ enum:
+ - 5xx
+ - gateway-error
+ - connect-failure
+ - retriable-4xx
+ - refused-stream
+ - retriable-status-codes
+ type: string
+ type: object
+ rewrite:
+ type: string
+ service:
+ type: string
+ shadow:
+ type: boolean
+ stats_name:
+ type: string
+ timeout_ms:
+ description: The timeout for requests that use this Mapping. Overrides
+ `cluster_request_timeout_ms` set on the Ambassador Module, if it
+ exists.
+ type: integer
+ tls:
+ type: string
+ use_websocket:
+ description: 'use_websocket is deprecated, and is equivlaent to setting
+ `allow_upgrade: ["websocket"]`'
+ type: boolean
+ v2BoolHeaders:
+ items:
+ type: string
+ type: array
+ v2BoolQueryParameters:
+ items:
+ type: string
+ type: array
+ # TODO: uncomment when [bug](https://github.com/fabric8io/kubernetes-client/issues/5457) is resolved
+ # v2ExplicitTLS:
+ # description: V2ExplicitTLS controls some vanity/stylistic elements
+ # when converting from v3alpha1 to v2. The values in an V2ExplicitTLS
+ # should not in any way affect the runtime operation of Emissary;
+ # except that it may affect internal names in the Envoy config, which
+ # may in turn affect stats names. But it should not affect any end-user
+ # observable behavior.
+ # properties:
+ # serviceScheme:
+ # description: "ServiceScheme specifies how to spell and capitalize
+ # the scheme-part of the service URL. \n Acceptable values are
+ # \"http://\" (case-insensitive), \"https://\" (case-insensitive),
+ # or \"\". The value is used if it agrees with whether or not
+ # this resource enables TLS origination, or if something else
+ # in the resource overrides the scheme."
+ # pattern: ^([hH][tT][tT][pP][sS]?://)?$
+ # type: string
+ # tls:
+ # description: "TLS controls whether and how to represent the \"tls\"
+ # field when its value could be implied by the \"service\" field.
+ # \ In v2, there were a lot of different ways to spell an \"empty\"
+ # value, and this field specifies which way to spell it (and will
+ # therefore only be used if the value will indeed be empty). \n
+ # | Value | Representation | Meaning
+ # of representation | |--------------+---------------------------------------+------------------------------------|
+ # | \"\" | omit the field | defer
+ # to service (no TLSContext) | | \"null\" | store an explicit
+ # \"null\" in the field | defer to service (no TLSContext) |
+ # | \"string\" | store an empty string in the field | defer
+ # to service (no TLSContext) | | \"bool:false\" | store a Boolean
+ # \"false\" in the field | defer to service (no TLSContext) |
+ # | \"bool:true\" | store a Boolean \"true\" in the field |
+ # originate TLS (no TLSContext) | \n If the meaning of the
+ # representation contradicts anything else (if a TLSContext is
+ # to be used, or in the case of \"bool:true\" if TLS is not to
+ # be originated), then this field is ignored."
+ # enum:
+ # - ""
+ # - "null"
+ # - bool:true
+ # - bool:false
+ # - string
+ # type: string
+ # type: object
+ weight:
+ type: integer
+ required:
+ - prefix
+ - service
+ type: object
+ status:
+ description: MappingStatus defines the observed state of Mapping
+ properties:
+ reason:
+ type: string
+ state:
+ enum:
+ - ""
+ - Inactive
+ - Running
+ type: string
+ type: object
+ type: object
+ served: true
+ storage: false
+ subresources:
+ status: {}
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.12.0
+ labels:
+ app.kubernetes.io/instance: emissary-apiext
+ app.kubernetes.io/managed-by: kubectl_apply_-f_emissary-apiext.yaml
+ app.kubernetes.io/name: emissary-apiext
+ app.kubernetes.io/part-of: emissary-apiext
+ name: tlscontexts.getambassador.io
+spec:
+ conversion:
+ strategy: Webhook
+ webhook:
+ clientConfig:
+ service:
+ name: emissary-apiext
+ namespace: emissary-system
+ conversionReviewVersions:
+ - v1
+ group: getambassador.io
+ names:
+ categories:
+ - ambassador-crds
+ kind: TLSContext
+ listKind: TLSContextList
+ plural: tlscontexts
+ singular: tlscontext
+ preserveUnknownFields: false
+ scope: Namespaced
+ versions:
+ - name: v1
+ schema:
+ openAPIV3Schema:
+ description: TLSContext is the Schema for the tlscontexts API
+ properties:
+ apiVersion:
+ description: 'APIVersion defines the versioned schema of this representation
+ of an object. Servers should convert recognized schemas to the latest
+ internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+ type: string
+ kind:
+ description: 'Kind is a string value representing the REST resource this
+ object represents. Servers may infer this from the endpoint the client
+ submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+ type: string
+ metadata:
+ type: object
+ spec:
+ description: TLSContextSpec defines the desired state of TLSContext
+ properties:
+ alpn_protocols:
+ type: string
+ ca_secret:
+ type: string
+ cacert_chain_file:
+ type: string
+ cert_chain_file:
+ type: string
+ cert_required:
+ type: boolean
+ cipher_suites:
+ items:
+ type: string
+ type: array
+ ecdh_curves:
+ items:
+ type: string
+ type: array
+ hosts:
+ items:
+ type: string
+ type: array
+ max_tls_version:
+ enum:
+ - v1.0
+ - v1.1
+ - v1.2
+ - v1.3
+ type: string
+ min_tls_version:
+ enum:
+ - v1.0
+ - v1.1
+ - v1.2
+ - v1.3
+ type: string
+ private_key_file:
+ type: string
+ redirect_cleartext_from:
+ type: integer
+ secret:
+ type: string
+ secret_namespacing:
+ type: boolean
+ sni:
+ type: string
+ v3CRLSecret:
+ type: string
+ type: object
+ x-kubernetes-preserve-unknown-fields: true
+ type: object
+ served: true
+ storage: false
+ - name: v2
+ schema:
+ openAPIV3Schema:
+ description: TLSContext is the Schema for the tlscontexts API
+ properties:
+ apiVersion:
+ description: 'APIVersion defines the versioned schema of this representation
+ of an object. Servers should convert recognized schemas to the latest
+ internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+ type: string
+ kind:
+ description: 'Kind is a string value representing the REST resource this
+ object represents. Servers may infer this from the endpoint the client
+ submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+ type: string
+ metadata:
+ type: object
+ spec:
+ description: TLSContextSpec defines the desired state of TLSContext
+ properties:
+ alpn_protocols:
+ type: string
+ # [operator] added manually by coping over from v3alpha1
+ ambassador_id:
+ description: "AmbassadorID declares which Ambassador instances should
+ pay attention to this resource. If no value is provided, the default
+ is: \n ambassador_id: - \"default\""
+ items:
+ type: string
+ type: array
+ ca_secret:
+ type: string
+ cacert_chain_file:
+ type: string
+ cert_chain_file:
+ type: string
+ cert_required:
+ type: boolean
+ cipher_suites:
+ items:
+ type: string
+ type: array
+ ecdh_curves:
+ items:
+ type: string
+ type: array
+ hosts:
+ items:
+ type: string
+ type: array
+ max_tls_version:
+ enum:
+ - v1.0
+ - v1.1
+ - v1.2
+ - v1.3
+ type: string
+ min_tls_version:
+ enum:
+ - v1.0
+ - v1.1
+ - v1.2
+ - v1.3
+ type: string
+ private_key_file:
+ type: string
+ redirect_cleartext_from:
+ type: integer
+ secret:
+ type: string
+ secret_namespacing:
+ type: boolean
+ sni:
+ type: string
+ v3CRLSecret:
+ type: string
+ type: object
+ x-kubernetes-preserve-unknown-fields: true
+ type: object
+ served: true
+ storage: true
+ - name: v3alpha1
+ schema:
+ openAPIV3Schema:
+ description: TLSContext is the Schema for the tlscontexts API
+ properties:
+ apiVersion:
+ description: 'APIVersion defines the versioned schema of this representation
+ of an object. Servers should convert recognized schemas to the latest
+ internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+ type: string
+ kind:
+ description: 'Kind is a string value representing the REST resource this
+ object represents. Servers may infer this from the endpoint the client
+ submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+ type: string
+ metadata:
+ type: object
+ spec:
+ description: TLSContextSpec defines the desired state of TLSContext
+ properties:
+ alpn_protocols:
+ type: string
+ ambassador_id:
+ description: "AmbassadorID declares which Ambassador instances should
+ pay attention to this resource. If no value is provided, the default
+ is: \n ambassador_id: - \"default\""
+ items:
+ type: string
+ type: array
+ ca_secret:
+ type: string
+ cacert_chain_file:
+ type: string
+ cert_chain_file:
+ type: string
+ cert_required:
+ type: boolean
+ cipher_suites:
+ items:
+ type: string
+ type: array
+ crl_secret:
+ type: string
+ ecdh_curves:
+ items:
+ type: string
+ type: array
+ hosts:
+ items:
+ type: string
+ type: array
+ max_tls_version:
+ enum:
+ - v1.0
+ - v1.1
+ - v1.2
+ - v1.3
+ type: string
+ min_tls_version:
+ enum:
+ - v1.0
+ - v1.1
+ - v1.2
+ - v1.3
+ type: string
+ private_key_file:
+ type: string
+ redirect_cleartext_from:
+ type: integer
+ secret:
+ type: string
+ secret_namespacing:
+ type: boolean
+ sni:
+ type: string
+ type: object
+ type: object
+ served: true
+ storage: false
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.12.0
+ labels:
+ app.kubernetes.io/instance: emissary-apiext
+ app.kubernetes.io/managed-by: kubectl_apply_-f_emissary-apiext.yaml
+ app.kubernetes.io/name: emissary-apiext
+ app.kubernetes.io/part-of: emissary-apiext
+ name: hosts.getambassador.io
+spec:
+ conversion:
+ strategy: Webhook
+ webhook:
+ clientConfig:
+ service:
+ name: emissary-apiext
+ namespace: emissary-system
+ conversionReviewVersions:
+ - v1
+ group: getambassador.io
+ names:
+ categories:
+ - ambassador-crds
+ kind: Host
+ listKind: HostList
+ plural: hosts
+ singular: host
+ preserveUnknownFields: false
+ scope: Namespaced
+ versions:
+ - additionalPrinterColumns:
+ - jsonPath: .spec.hostname
+ name: Hostname
+ type: string
+ - jsonPath: .status.state
+ name: State
+ type: string
+ - jsonPath: .status.phaseCompleted
+ name: Phase Completed
+ type: string
+ - jsonPath: .status.phasePending
+ name: Phase Pending
+ type: string
+ - jsonPath: .metadata.creationTimestamp
+ name: Age
+ type: date
+ name: v2
+ schema:
+ openAPIV3Schema:
+ description: Host is the Schema for the hosts API
+ properties:
+ apiVersion:
+ description: 'APIVersion defines the versioned schema of this representation
+ of an object. Servers should convert recognized schemas to the latest
+ internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+ type: string
+ kind:
+ description: 'Kind is a string value representing the REST resource this
+ object represents. Servers may infer this from the endpoint the client
+ submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+ type: string
+ metadata:
+ type: object
+ spec:
+ description: HostSpec defines the desired state of Host
+ properties:
+ acmeProvider:
+ description: Specifies whether/who to talk ACME with to automatically
+ manage the $tlsSecret.
+ properties:
+ authority:
+ description: Specifies who to talk ACME with to get certs. Defaults
+ to Let's Encrypt; if "none" (case-insensitive), do not try to
+ do ACME for this Host.
+ type: string
+ email:
+ type: string
+ privateKeySecret:
+ description: "Specifies the Kubernetes Secret to use to store
+ the private key of the ACME account (essentially, where to store
+ the auto-generated password for the auto-created ACME account).
+ \ You should not normally need to set this--the default value
+ is based on a combination of the ACME authority being registered
+ wit and the email address associated with the account. \n Note
+ that this is a native-Kubernetes-style core.v1.LocalObjectReference,
+ not an Ambassador-style `{name}.{namespace}` string. Because
+ we're opinionated, it does not support referencing a Secret
+ in another namespace (because most native Kubernetes resources
+ don't support that), but if we ever abandon that opinion and
+ decide to support non-local references it, it would be by adding
+ a `namespace:` field by changing it from a core.v1.LocalObjectReference
+ to a core.v1.SecretReference, not by adopting the `{name}.{namespace}`
+ notation."
+ properties:
+ name:
+ description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+ TODO: Add other useful fields. apiVersion, kind, uid?'
+ type: string
+ type: object
+ x-kubernetes-map-type: atomic
+ registration:
+ description: This is normally set automatically
+ type: string
+ type: object
+ ambassador_id:
+ description: Common to all Ambassador objects (and optional).
+ items:
+ type: string
+ type: array
+ hostname:
+ description: Hostname by which the Ambassador can be reached.
+ type: string
+ previewUrl:
+ description: Configuration for the Preview URL feature of Service
+ Preview. Defaults to preview URLs not enabled.
+ properties:
+ enabled:
+ description: Is the Preview URL feature enabled?
+ type: boolean
+ type:
+ description: What type of Preview URL is allowed?
+ enum:
+ - Path
+ type: string
+ type: object
+ requestPolicy:
+ description: Request policy definition.
+ properties:
+ insecure:
+ properties:
+ action:
+ enum:
+ - Redirect
+ - Reject
+ - Route
+ type: string
+ additionalPort:
+ type: integer
+ type: object
+ type: object
+ selector:
+ description: Selector by which we can find further configuration.
+ Defaults to hostname=$hostname
+ properties:
+ matchExpressions:
+ description: matchExpressions is a list of label selector requirements.
+ The requirements are ANDed.
+ items:
+ description: A label selector requirement is a selector that
+ contains values, a key, and an operator that relates the key
+ and values.
+ properties:
+ key:
+ description: key is the label key that the selector applies
+ to.
+ type: string
+ operator:
+ description: operator represents a key's relationship to
+ a set of values. Valid operators are In, NotIn, Exists
+ and DoesNotExist.
+ type: string
+ values:
+ description: values is an array of string values. If the
+ operator is In or NotIn, the values array must be non-empty.
+ If the operator is Exists or DoesNotExist, the values
+ array must be empty. This array is replaced during a strategic
+ merge patch.
+ items:
+ type: string
+ type: array
+ required:
+ - key
+ - operator
+ type: object
+ type: array
+ matchLabels:
+ additionalProperties:
+ type: string
+ description: matchLabels is a map of {key,value} pairs. A single
+ {key,value} in the matchLabels map is equivalent to an element
+ of matchExpressions, whose key field is "key", the operator
+ is "In", and the values array contains only "value". The requirements
+ are ANDed.
+ type: object
+ type: object
+ x-kubernetes-map-type: atomic
+ tls:
+ description: TLS configuration. It is not valid to specify both `tlsContext`
+ and `tls`.
+ properties:
+ alpn_protocols:
+ type: string
+ ca_secret:
+ type: string
+ cacert_chain_file:
+ type: string
+ cert_chain_file:
+ type: string
+ cert_required:
+ type: boolean
+ cipher_suites:
+ items:
+ type: string
+ type: array
+ ecdh_curves:
+ items:
+ type: string
+ type: array
+ max_tls_version:
+ type: string
+ min_tls_version:
+ type: string
+ private_key_file:
+ type: string
+ redirect_cleartext_from:
+ type: integer
+ sni:
+ type: string
+ v3CRLSecret:
+ type: string
+ type: object
+ tlsContext:
+ description: "Name of the TLSContext the Host resource is linked with.
+ It is not valid to specify both `tlsContext` and `tls`. \n Note
+ that this is a native-Kubernetes-style core.v1.LocalObjectReference,
+ not an Ambassador-style `{name}.{namespace}` string. Because we're
+ opinionated, it does not support referencing a Secret in another
+ namespace (because most native Kubernetes resources don't support
+ that), but if we ever abandon that opinion and decide to support
+ non-local references it, it would be by adding a `namespace:` field
+ by changing it from a core.v1.LocalObjectReference to a core.v1.SecretReference,
+ not by adopting the `{name}.{namespace}` notation."
+ properties:
+ name:
+ description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+ TODO: Add other useful fields. apiVersion, kind, uid?'
+ type: string
+ type: object
+ x-kubernetes-map-type: atomic
+ tlsSecret:
+ description: Name of the Kubernetes secret into which to save generated
+ certificates. If ACME is enabled (see $acmeProvider), then the
+ default is $hostname; otherwise the default is "". If the value
+ is "", then we do not do TLS for this Host.
+ properties:
+ name:
+ description: name is unique within a namespace to reference a
+ secret resource.
+ type: string
+ namespace:
+ description: namespace defines the space within which the secret
+ name must be unique.
+ type: string
+ type: object
+ x-kubernetes-map-type: atomic
+ type: object
+ x-kubernetes-preserve-unknown-fields: true
+ status:
+ description: HostStatus defines the observed state of Host
+ properties:
+ errorBackoff:
+ type: string
+ errorReason:
+ description: errorReason, errorTimestamp, and errorBackoff are valid
+ when state==Error.
+ type: string
+ errorTimestamp:
+ format: date-time
+ type: string
+ phaseCompleted:
+ description: phaseCompleted and phasePending are valid when state==Pending
+ or state==Error.
+ enum:
+ - NA
+ - DefaultsFilled
+ - ACMEUserPrivateKeyCreated
+ - ACMEUserRegistered
+ - ACMECertificateChallenge
+ type: string
+ phasePending:
+ description: phaseCompleted and phasePending are valid when state==Pending
+ or state==Error.
+ enum:
+ - NA
+ - DefaultsFilled
+ - ACMEUserPrivateKeyCreated
+ - ACMEUserRegistered
+ - ACMECertificateChallenge
+ type: string
+ state:
+ enum:
+ - Initial
+ - Pending
+ - Ready
+ - Error
+ type: string
+ tlsCertificateSource:
+ enum:
+ - Unknown
+ - None
+ - Other
+ - ACME
+ type: string
+ type: object
+ type: object
+ served: true
+ storage: true
+ subresources:
+ status: {}
+ - additionalPrinterColumns:
+ - jsonPath: .spec.hostname
+ name: Hostname
+ type: string
+ - jsonPath: .status.state
+ name: State
+ type: string
+ - jsonPath: .status.phaseCompleted
+ name: Phase Completed
+ type: string
+ - jsonPath: .status.phasePending
+ name: Phase Pending
+ type: string
+ - jsonPath: .metadata.creationTimestamp
+ name: Age
+ type: date
+ name: v3alpha1
+ schema:
+ openAPIV3Schema:
+ description: Host is the Schema for the hosts API
+ properties:
+ apiVersion:
+ description: 'APIVersion defines the versioned schema of this representation
+ of an object. Servers should convert recognized schemas to the latest
+ internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+ type: string
+ kind:
+ description: 'Kind is a string value representing the REST resource this
+ object represents. Servers may infer this from the endpoint the client
+ submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+ type: string
+ metadata:
+ type: object
+ spec:
+ description: HostSpec defines the desired state of Host
+ properties:
+ acmeProvider:
+ description: Specifies whether/who to talk ACME with to automatically
+ manage the $tlsSecret.
+ properties:
+ authority:
+ description: Specifies who to talk ACME with to get certs. Defaults
+ to Let's Encrypt; if "none" (case-insensitive), do not try to
+ do ACME for this Host.
+ type: string
+ email:
+ type: string
+ privateKeySecret:
+ description: "Specifies the Kubernetes Secret to use to store
+ the private key of the ACME account (essentially, where to store
+ the auto-generated password for the auto-created ACME account).
+ \ You should not normally need to set this--the default value
+ is based on a combination of the ACME authority being registered
+ wit and the email address associated with the account. \n Note
+ that this is a native-Kubernetes-style core.v1.LocalObjectReference,
+ not an Ambassador-style `{name}.{namespace}` string. Because
+ we're opinionated, it does not support referencing a Secret
+ in another namespace (because most native Kubernetes resources
+ don't support that), but if we ever abandon that opinion and
+ decide to support non-local references it, it would be by adding
+ a `namespace:` field by changing it from a core.v1.LocalObjectReference
+ to a core.v1.SecretReference, not by adopting the `{name}.{namespace}`
+ notation."
+ properties:
+ name:
+ description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+ TODO: Add other useful fields. apiVersion, kind, uid?'
+ type: string
+ type: object
+ x-kubernetes-map-type: atomic
+ registration:
+ description: This is normally set automatically
+ type: string
+ type: object
+ ambassador_id:
+ description: Common to all Ambassador objects (and optional).
+ items:
+ type: string
+ type: array
+ hostname:
+ description: Hostname by which the Ambassador can be reached.
+ type: string
+ mappingSelector:
+ description: Selector for Mappings we'll associate with this Host.
+ At the moment, Selector and MappingSelector are synonyms, but that
+ will change soon.
+ properties:
+ matchExpressions:
+ description: matchExpressions is a list of label selector requirements.
+ The requirements are ANDed.
+ items:
+ description: A label selector requirement is a selector that
+ contains values, a key, and an operator that relates the key
+ and values.
+ properties:
+ key:
+ description: key is the label key that the selector applies
+ to.
+ type: string
+ operator:
+ description: operator represents a key's relationship to
+ a set of values. Valid operators are In, NotIn, Exists
+ and DoesNotExist.
+ type: string
+ values:
+ description: values is an array of string values. If the
+ operator is In or NotIn, the values array must be non-empty.
+ If the operator is Exists or DoesNotExist, the values
+ array must be empty. This array is replaced during a strategic
+ merge patch.
+ items:
+ type: string
+ type: array
+ required:
+ - key
+ - operator
+ type: object
+ type: array
+ matchLabels:
+ additionalProperties:
+ type: string
+ description: matchLabels is a map of {key,value} pairs. A single
+ {key,value} in the matchLabels map is equivalent to an element
+ of matchExpressions, whose key field is "key", the operator
+ is "In", and the values array contains only "value". The requirements
+ are ANDed.
+ type: object
+ type: object
+ x-kubernetes-map-type: atomic
+ previewUrl:
+ description: Configuration for the Preview URL feature of Service
+ Preview. Defaults to preview URLs not enabled.
+ properties:
+ enabled:
+ description: Is the Preview URL feature enabled?
+ type: boolean
+ type:
+ description: What type of Preview URL is allowed?
+ enum:
+ - Path
+ type: string
+ type: object
+ requestPolicy:
+ description: Request policy definition.
+ properties:
+ insecure:
+ properties:
+ action:
+ enum:
+ - Redirect
+ - Reject
+ - Route
+ type: string
+ additionalPort:
+ type: integer
+ type: object
+ type: object
+ selector:
+ description: 'DEPRECATED: Selector by which we can find further configuration.
+ Use MappingSelector instead.'
+ properties:
+ matchExpressions:
+ description: matchExpressions is a list of label selector requirements.
+ The requirements are ANDed.
+ items:
+ description: A label selector requirement is a selector that
+ contains values, a key, and an operator that relates the key
+ and values.
+ properties:
+ key:
+ description: key is the label key that the selector applies
+ to.
+ type: string
+ operator:
+ description: operator represents a key's relationship to
+ a set of values. Valid operators are In, NotIn, Exists
+ and DoesNotExist.
+ type: string
+ values:
+ description: values is an array of string values. If the
+ operator is In or NotIn, the values array must be non-empty.
+ If the operator is Exists or DoesNotExist, the values
+ array must be empty. This array is replaced during a strategic
+ merge patch.
+ items:
+ type: string
+ type: array
+ required:
+ - key
+ - operator
+ type: object
+ type: array
+ matchLabels:
+ additionalProperties:
+ type: string
+ description: matchLabels is a map of {key,value} pairs. A single
+ {key,value} in the matchLabels map is equivalent to an element
+ of matchExpressions, whose key field is "key", the operator
+ is "In", and the values array contains only "value". The requirements
+ are ANDed.
+ type: object
+ type: object
+ x-kubernetes-map-type: atomic
+ tls:
+ description: TLS configuration. It is not valid to specify both `tlsContext`
+ and `tls`.
+ properties:
+ alpn_protocols:
+ type: string
+ ca_secret:
+ type: string
+ cacert_chain_file:
+ type: string
+ cert_chain_file:
+ type: string
+ cert_required:
+ type: boolean
+ cipher_suites:
+ items:
+ type: string
+ type: array
+ crl_secret:
+ type: string
+ ecdh_curves:
+ items:
+ type: string
+ type: array
+ max_tls_version:
+ type: string
+ min_tls_version:
+ type: string
+ private_key_file:
+ type: string
+ redirect_cleartext_from:
+ type: integer
+ sni:
+ type: string
+ type: object
+ tlsContext:
+ description: "Name of the TLSContext the Host resource is linked with.
+ It is not valid to specify both `tlsContext` and `tls`. \n Note
+ that this is a native-Kubernetes-style core.v1.LocalObjectReference,
+ not an Ambassador-style `{name}.{namespace}` string. Because we're
+ opinionated, it does not support referencing a Secret in another
+ namespace (because most native Kubernetes resources don't support
+ that), but if we ever abandon that opinion and decide to support
+ non-local references it, it would be by adding a `namespace:` field
+ by changing it from a core.v1.LocalObjectReference to a core.v1.SecretReference,
+ not by adopting the `{name}.{namespace}` notation."
+ properties:
+ name:
+ description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+ TODO: Add other useful fields. apiVersion, kind, uid?'
+ type: string
+ type: object
+ x-kubernetes-map-type: atomic
+ tlsSecret:
+ description: Name of the Kubernetes secret into which to save generated
+ certificates. If ACME is enabled (see $acmeProvider), then the
+ default is $hostname; otherwise the default is "". If the value
+ is "", then we do not do TLS for this Host.
+ properties:
+ name:
+ description: name is unique within a namespace to reference a
+ secret resource.
+ type: string
+ namespace:
+ description: namespace defines the space within which the secret
+ name must be unique.
+ type: string
+ type: object
+ x-kubernetes-map-type: atomic
+ type: object
+ status:
+ description: HostStatus defines the observed state of Host
+ properties:
+ errorBackoff:
+ type: string
+ errorReason:
+ description: errorReason, errorTimestamp, and errorBackoff are valid
+ when state==Error.
+ type: string
+ errorTimestamp:
+ format: date-time
+ type: string
+ phaseCompleted:
+ description: phaseCompleted and phasePending are valid when state==Pending
+ or state==Error.
+ enum:
+ - NA
+ - DefaultsFilled
+ - ACMEUserPrivateKeyCreated
+ - ACMEUserRegistered
+ - ACMECertificateChallenge
+ type: string
+ phasePending:
+ description: phaseCompleted and phasePending are valid when state==Pending
+ or state==Error.
+ enum:
+ - NA
+ - DefaultsFilled
+ - ACMEUserPrivateKeyCreated
+ - ACMEUserRegistered
+ - ACMECertificateChallenge
+ type: string
+ state:
+ enum:
+ - Initial
+ - Pending
+ - Ready
+ - Error
+ type: string
+ tlsCertificateSource:
+ enum:
+ - Unknown
+ - None
+ - Other
+ - ACME
+ type: string
+ type: object
+ type: object
+ served: true
+ storage: false
+ subresources:
+ status: {}
diff --git a/charts/k8s-gerrit/operator/src/main/resources/log4j2.xml b/charts/k8s-gerrit/operator/src/main/resources/log4j2.xml
new file mode 100644
index 0000000..f3dd273
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/main/resources/log4j2.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration status="INFO">
+ <Appenders>
+ <Console name="Console" target="SYSTEM_OUT">
+ <PatternLayout pattern="%d{HH:mm:ss.SSS} [%-5level] %c:%L [PID:%pid] - %msg%n"/>
+ </Console>
+ </Appenders>
+ <Loggers>
+ <Root level="info">
+ <AppenderRef ref="Console"/>
+ </Root>
+ </Loggers>
+</Configuration>
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/cluster/GerritClusterE2E.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/cluster/GerritClusterE2E.java
new file mode 100644
index 0000000..b73f463
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/cluster/GerritClusterE2E.java
@@ -0,0 +1,74 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.cluster;
+
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.notNullValue;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.k8s.operator.cluster.dependent.NfsIdmapdConfigMap;
+import com.google.gerrit.k8s.operator.cluster.dependent.SharedPVC;
+import com.google.gerrit.k8s.operator.network.IngressType;
+import com.google.gerrit.k8s.operator.test.AbstractGerritOperatorE2ETest;
+import io.fabric8.kubernetes.api.model.ConfigMap;
+import io.fabric8.kubernetes.api.model.PersistentVolumeClaim;
+import org.junit.jupiter.api.Test;
+
+public class GerritClusterE2E extends AbstractGerritOperatorE2ETest {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ @Test
+ void testSharedPvcCreated() {
+ logger.atInfo().log("Waiting max 1 minutes for the shared pvc to be created.");
+ await()
+ .atMost(1, MINUTES)
+ .untilAsserted(
+ () -> {
+ PersistentVolumeClaim pvc =
+ client
+ .persistentVolumeClaims()
+ .inNamespace(operator.getNamespace())
+ .withName(SharedPVC.SHARED_PVC_NAME)
+ .get();
+ assertThat(pvc, is(notNullValue()));
+ });
+ }
+
+ @Test
+ void testNfsIdmapdConfigMapCreated() {
+ gerritCluster.setNfsEnabled(true);
+ logger.atInfo().log("Waiting max 1 minutes for the nfs idmapd configmap to be created.");
+ await()
+ .atMost(1, MINUTES)
+ .untilAsserted(
+ () -> {
+ ConfigMap cm =
+ client
+ .configMaps()
+ .inNamespace(operator.getNamespace())
+ .withName(NfsIdmapdConfigMap.NFS_IDMAPD_CM_NAME)
+ .get();
+ assertThat(cm, is(notNullValue()));
+ });
+ }
+
+ @Override
+ protected IngressType getIngressType() {
+ return IngressType.INGRESS;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/cluster/GerritRepositoryConfigTest.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/cluster/GerritRepositoryConfigTest.java
new file mode 100644
index 0000000..5474780
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/cluster/GerritRepositoryConfigTest.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.cluster;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GerritRepositoryConfig;
+import org.junit.jupiter.api.Test;
+
+public class GerritRepositoryConfigTest {
+
+ @Test
+ public void testFullImageNameComputesCorrectly() {
+ assertThat(
+ new GerritRepositoryConfig().getFullImageName("gerrit"),
+ is(equalTo("docker.io/k8sgerrit/gerrit:latest")));
+
+ GerritRepositoryConfig repoConfig1 = new GerritRepositoryConfig();
+ repoConfig1.setOrg("testorg");
+ repoConfig1.setRegistry("registry.example.com");
+ repoConfig1.setTag("v1.0");
+ assertThat(
+ repoConfig1.getFullImageName("gerrit"),
+ is(equalTo("registry.example.com/testorg/gerrit:v1.0")));
+
+ GerritRepositoryConfig repoConfig2 = new GerritRepositoryConfig();
+ repoConfig2.setOrg(null);
+ repoConfig2.setRegistry(null);
+ repoConfig2.setTag(null);
+ assertThat(repoConfig2.getFullImageName("gerrit"), is(equalTo("gerrit")));
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/ClusterManagedGerritWithIngressE2E.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/ClusterManagedGerritWithIngressE2E.java
new file mode 100644
index 0000000..c848aa9
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/ClusterManagedGerritWithIngressE2E.java
@@ -0,0 +1,140 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gerrit;
+
+import static com.google.gerrit.k8s.operator.network.ingress.dependent.GerritClusterIngress.INGRESS_NAME;
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.k8s.operator.network.IngressType;
+import com.google.gerrit.k8s.operator.test.AbstractGerritOperatorE2ETest;
+import com.google.gerrit.k8s.operator.test.TestGerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplate;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplateSpec.GerritMode;
+import io.fabric8.kubernetes.api.model.networking.v1.Ingress;
+import io.fabric8.kubernetes.api.model.networking.v1.IngressLoadBalancerIngress;
+import io.fabric8.kubernetes.api.model.networking.v1.IngressStatus;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+public class ClusterManagedGerritWithIngressE2E extends AbstractGerritOperatorE2ETest {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ @Test
+ void testPrimaryGerritIsCreated() throws Exception {
+ TestGerrit gerrit =
+ new TestGerrit(client, testProps, GerritMode.PRIMARY, "gerrit", operator.getNamespace());
+ GerritTemplate gerritTemplate = gerrit.createGerritTemplate();
+ gerritCluster.addGerrit(gerritTemplate);
+ gerritCluster.deploy();
+
+ logger.atInfo().log("Waiting max 2 minutes for the Ingress to have an external IP.");
+ await()
+ .atMost(2, MINUTES)
+ .untilAsserted(
+ () -> {
+ Ingress ingress =
+ client
+ .network()
+ .v1()
+ .ingresses()
+ .inNamespace(operator.getNamespace())
+ .withName(INGRESS_NAME)
+ .get();
+ assertThat(ingress, is(notNullValue()));
+ IngressStatus status = ingress.getStatus();
+ assertThat(status, is(notNullValue()));
+ List<IngressLoadBalancerIngress> lbIngresses = status.getLoadBalancer().getIngress();
+ assertThat(lbIngresses, hasSize(1));
+ assertThat(lbIngresses.get(0).getIp(), is(notNullValue()));
+ });
+
+ GerritApi gerritApi = gerritCluster.getGerritApiClient(gerritTemplate, IngressType.INGRESS);
+ await()
+ .atMost(2, MINUTES)
+ .untilAsserted(
+ () -> {
+ assertDoesNotThrow(() -> gerritApi.config().server().getVersion());
+ assertThat(gerritApi.config().server().getVersion(), notNullValue());
+ assertThat(gerritApi.config().server().getVersion(), not(is("<2.8")));
+ logger.atInfo().log("Gerrit version: %s", gerritApi.config().server().getVersion());
+ });
+ }
+
+ @Test
+ void testGerritReplicaIsCreated() throws Exception {
+ String gerritName = "gerrit-replica";
+ TestGerrit gerrit =
+ new TestGerrit(client, testProps, GerritMode.REPLICA, gerritName, operator.getNamespace());
+ gerritCluster.addGerrit(gerrit.createGerritTemplate());
+ gerritCluster.deploy();
+
+ assertTrue(
+ client
+ .pods()
+ .inNamespace(operator.getNamespace())
+ .withName(gerritName + "-0")
+ .inContainer("gerrit")
+ .getLog()
+ .contains("Gerrit Code Review [replica]"));
+ }
+
+ @Test
+ void testGerritReplicaAndPrimaryGerritAreCreated() throws Exception {
+ String primaryGerritName = "gerrit";
+ TestGerrit primaryGerrit =
+ new TestGerrit(
+ client, testProps, GerritMode.PRIMARY, primaryGerritName, operator.getNamespace());
+ gerritCluster.addGerrit(primaryGerrit.createGerritTemplate());
+ String gerritReplicaName = "gerrit-replica";
+ TestGerrit gerritReplica =
+ new TestGerrit(
+ client, testProps, GerritMode.REPLICA, gerritReplicaName, operator.getNamespace());
+ gerritCluster.addGerrit(gerritReplica.createGerritTemplate());
+ gerritCluster.deploy();
+
+ assertTrue(
+ client
+ .pods()
+ .inNamespace(operator.getNamespace())
+ .withName(primaryGerritName + "-0")
+ .inContainer("gerrit")
+ .getLog()
+ .contains("Gerrit Code Review"));
+
+ assertTrue(
+ client
+ .pods()
+ .inNamespace(operator.getNamespace())
+ .withName(gerritReplicaName + "-0")
+ .inContainer("gerrit")
+ .getLog()
+ .contains("Gerrit Code Review [replica]"));
+ }
+
+ @Override
+ protected IngressType getIngressType() {
+ return IngressType.INGRESS;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/ClusterManagedGerritWithIstioE2E.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/ClusterManagedGerritWithIstioE2E.java
new file mode 100644
index 0000000..fdf5ccb
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/ClusterManagedGerritWithIstioE2E.java
@@ -0,0 +1,144 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gerrit;
+
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.k8s.operator.network.IngressType;
+import com.google.gerrit.k8s.operator.test.AbstractGerritOperatorE2ETest;
+import com.google.gerrit.k8s.operator.test.TestGerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplate;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplateSpec.GerritMode;
+import org.junit.jupiter.api.Test;
+
+public class ClusterManagedGerritWithIstioE2E extends AbstractGerritOperatorE2ETest {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ @Test
+ void testPrimaryGerritWithIstio() throws Exception {
+ GerritTemplate gerrit =
+ new TestGerrit(client, testProps, GerritMode.PRIMARY, "gerrit", operator.getNamespace())
+ .createGerritTemplate();
+ gerritCluster.addGerrit(gerrit);
+ gerritCluster.deploy();
+
+ GerritApi gerritApi = gerritCluster.getGerritApiClient(gerrit, IngressType.ISTIO);
+ await()
+ .atMost(2, MINUTES)
+ .untilAsserted(
+ () -> {
+ assertDoesNotThrow(() -> gerritApi.config().server().getVersion());
+ assertThat(gerritApi.config().server().getVersion(), notNullValue());
+ assertThat(gerritApi.config().server().getVersion(), not(is("<2.8")));
+ logger.atInfo().log("Gerrit version: %s", gerritApi.config().server().getVersion());
+ });
+ }
+
+ @Test
+ void testGerritReplicaIsCreated() throws Exception {
+ String gerritName = "gerrit-replica";
+ TestGerrit gerrit =
+ new TestGerrit(client, testProps, GerritMode.REPLICA, gerritName, operator.getNamespace());
+ gerritCluster.addGerrit(gerrit.createGerritTemplate());
+ gerritCluster.deploy();
+
+ assertTrue(
+ client
+ .pods()
+ .inNamespace(operator.getNamespace())
+ .withName(gerritName + "-0")
+ .inContainer("gerrit")
+ .getLog()
+ .contains("Gerrit Code Review [replica]"));
+ }
+
+ @Test
+ void testMultipleGerritReplicaAreCreated() throws Exception {
+ String gerritName = "gerrit-replica-1";
+ TestGerrit gerrit =
+ new TestGerrit(client, testProps, GerritMode.REPLICA, gerritName, operator.getNamespace());
+ gerritCluster.addGerrit(gerrit.createGerritTemplate());
+ String gerritName2 = "gerrit-replica-2";
+ TestGerrit gerrit2 =
+ new TestGerrit(client, testProps, GerritMode.REPLICA, gerritName2, operator.getNamespace());
+ gerritCluster.addGerrit(gerrit2.createGerritTemplate());
+ gerritCluster.deploy();
+
+ assertTrue(
+ client
+ .pods()
+ .inNamespace(operator.getNamespace())
+ .withName(gerritName + "-0")
+ .inContainer("gerrit")
+ .getLog()
+ .contains("Gerrit Code Review [replica]"));
+
+ assertTrue(
+ client
+ .pods()
+ .inNamespace(operator.getNamespace())
+ .withName(gerritName2 + "-0")
+ .inContainer("gerrit")
+ .getLog()
+ .contains("Gerrit Code Review [replica]"));
+ }
+
+ @Test
+ void testGerritReplicaAndPrimaryGerritAreCreated() throws Exception {
+ String primaryGerritName = "gerrit";
+ TestGerrit primaryGerrit =
+ new TestGerrit(
+ client, testProps, GerritMode.PRIMARY, primaryGerritName, operator.getNamespace());
+ gerritCluster.addGerrit(primaryGerrit.createGerritTemplate());
+ String gerritReplicaName = "gerrit-replica";
+ TestGerrit gerritReplica =
+ new TestGerrit(
+ client, testProps, GerritMode.REPLICA, gerritReplicaName, operator.getNamespace());
+ gerritCluster.addGerrit(gerritReplica.createGerritTemplate());
+ gerritCluster.deploy();
+
+ assertTrue(
+ client
+ .pods()
+ .inNamespace(operator.getNamespace())
+ .withName(primaryGerritName + "-0")
+ .inContainer("gerrit")
+ .getLog()
+ .contains("Gerrit Code Review"));
+
+ assertTrue(
+ client
+ .pods()
+ .inNamespace(operator.getNamespace())
+ .withName(gerritReplicaName + "-0")
+ .inContainer("gerrit")
+ .getLog()
+ .contains("Gerrit Code Review [replica]"));
+ }
+
+ @Override
+ protected IngressType getIngressType() {
+ return IngressType.ISTIO;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/GerritConfigReconciliationE2E.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/GerritConfigReconciliationE2E.java
new file mode 100644
index 0000000..342d524
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/GerritConfigReconciliationE2E.java
@@ -0,0 +1,163 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gerrit;
+
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.gerrit.k8s.operator.network.IngressType;
+import com.google.gerrit.k8s.operator.test.AbstractGerritOperatorE2ETest;
+import com.google.gerrit.k8s.operator.test.TestGerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplate;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplateSpec;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplateSpec.GerritMode;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.HttpSshServiceConfig;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class GerritConfigReconciliationE2E extends AbstractGerritOperatorE2ETest {
+ private static final String GERRIT_NAME = "gerrit";
+ private static final String RESTART_ANNOTATION = "kubectl.kubernetes.io/restartedAt";
+
+ private GerritTemplate gerritTemplate;
+
+ @BeforeEach
+ public void setupGerrit() throws Exception {
+ TestGerrit gerrit =
+ new TestGerrit(client, testProps, GerritMode.PRIMARY, GERRIT_NAME, operator.getNamespace());
+ gerritTemplate = gerrit.createGerritTemplate();
+ gerritCluster.addGerrit(gerritTemplate);
+ gerritCluster.deploy();
+ }
+
+ @Test
+ void testNoRestartIfGerritConfigUnchanged() throws Exception {
+ Map<String, String> annotations = getReplicaSetAnnotations();
+ assertFalse(annotations.containsKey(RESTART_ANNOTATION));
+
+ gerritCluster.removeGerrit(gerritTemplate);
+ GerritTemplateSpec gerritSpec = gerritTemplate.getSpec();
+ HttpSshServiceConfig gerritServiceConfig = new HttpSshServiceConfig();
+ gerritServiceConfig.setHttpPort(48080);
+ gerritSpec.setService(gerritServiceConfig);
+ gerritTemplate.setSpec(gerritSpec);
+ gerritCluster.addGerrit(gerritTemplate);
+ gerritCluster.deploy();
+
+ await()
+ .atMost(30, SECONDS)
+ .untilAsserted(
+ () -> {
+ assertTrue(
+ client
+ .services()
+ .inNamespace(operator.getNamespace())
+ .withName(GERRIT_NAME)
+ .get()
+ .getSpec()
+ .getPorts()
+ .stream()
+ .allMatch(p -> p.getPort() == 48080));
+ assertFalse(getReplicaSetAnnotations().containsKey(RESTART_ANNOTATION));
+ });
+ }
+
+ @Test
+ void testRestartOnGerritConfigMapChange() throws Exception {
+ String podV1Uid =
+ client
+ .pods()
+ .inNamespace(operator.getNamespace())
+ .withName(GERRIT_NAME + "-0")
+ .get()
+ .getMetadata()
+ .getUid();
+
+ gerritCluster.removeGerrit(gerritTemplate);
+ GerritTemplateSpec gerritSpec = gerritTemplate.getSpec();
+ Map<String, String> cfgs = new HashMap<>();
+ cfgs.putAll(gerritSpec.getConfigFiles());
+ cfgs.put("test.config", "[test]\n test");
+ gerritSpec.setConfigFiles(cfgs);
+ gerritTemplate.setSpec(gerritSpec);
+ gerritCluster.addGerrit(gerritTemplate);
+ gerritCluster.deploy();
+
+ assertGerritRestart(podV1Uid);
+ }
+
+ @Test
+ void testRestartOnGerritSecretChange() throws Exception {
+ String podV1Uid =
+ client
+ .pods()
+ .inNamespace(operator.getNamespace())
+ .withName(GERRIT_NAME + "-0")
+ .get()
+ .getMetadata()
+ .getUid();
+
+ secureConfig.modify("test", "test", "test");
+
+ assertGerritRestart(podV1Uid);
+ }
+
+ private void assertGerritRestart(String uidOld) {
+ await()
+ .atMost(2, MINUTES)
+ .untilAsserted(
+ () -> {
+ assertTrue(
+ client
+ .pods()
+ .inNamespace(operator.getNamespace())
+ .withName(GERRIT_NAME + "-0")
+ .isReady());
+ assertTrue(getReplicaSetAnnotations().containsKey(RESTART_ANNOTATION));
+ assertFalse(
+ uidOld.equals(
+ client
+ .pods()
+ .inNamespace(operator.getNamespace())
+ .withName(GERRIT_NAME + "-0")
+ .get()
+ .getMetadata()
+ .getUid()));
+ });
+ }
+
+ private Map<String, String> getReplicaSetAnnotations() {
+ return client
+ .apps()
+ .statefulSets()
+ .inNamespace(operator.getNamespace())
+ .withName(GERRIT_NAME)
+ .get()
+ .getSpec()
+ .getTemplate()
+ .getMetadata()
+ .getAnnotations();
+ }
+
+ @Override
+ protected IngressType getIngressType() {
+ return IngressType.INGRESS;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/StandaloneGerritE2E.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/StandaloneGerritE2E.java
new file mode 100644
index 0000000..b7359ab
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/StandaloneGerritE2E.java
@@ -0,0 +1,64 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gerrit;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.gerrit.k8s.operator.network.IngressType;
+import com.google.gerrit.k8s.operator.test.AbstractGerritOperatorE2ETest;
+import com.google.gerrit.k8s.operator.test.TestGerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplateSpec.GerritMode;
+import org.junit.jupiter.api.Test;
+
+public class StandaloneGerritE2E extends AbstractGerritOperatorE2ETest {
+
+ @Test
+ void testPrimaryGerritIsCreated() throws Exception {
+ String gerritName = "gerrit";
+ TestGerrit testGerrit = new TestGerrit(client, testProps, gerritName, operator.getNamespace());
+ testGerrit.deploy();
+
+ assertTrue(
+ client
+ .pods()
+ .inNamespace(operator.getNamespace())
+ .withName(gerritName + "-0")
+ .inContainer("gerrit")
+ .getLog()
+ .contains("Gerrit Code Review"));
+ }
+
+ @Test
+ void testGerritReplicaIsCreated() throws Exception {
+ String gerritName = "gerrit-replica";
+ TestGerrit testGerrit =
+ new TestGerrit(client, testProps, GerritMode.REPLICA, gerritName, operator.getNamespace());
+ testGerrit.deploy();
+
+ assertTrue(
+ client
+ .pods()
+ .inNamespace(operator.getNamespace())
+ .withName(gerritName + "-0")
+ .inContainer("gerrit")
+ .getLog()
+ .contains("Gerrit Code Review [replica]"));
+ }
+
+ @Override
+ protected IngressType getIngressType() {
+ return IngressType.INGRESS;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/config/GerritConfigBuilderTest.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/config/GerritConfigBuilderTest.java
new file mode 100644
index 0000000..cedcebb
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/config/GerritConfigBuilderTest.java
@@ -0,0 +1,85 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gerrit.config;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritSpec;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.IngressConfig;
+import com.google.gerrit.k8s.operator.v1alpha.gerrit.config.GerritConfigBuilder;
+import java.util.Map;
+import java.util.Set;
+import org.assertj.core.util.Arrays;
+import org.eclipse.jgit.lib.Config;
+import org.junit.jupiter.api.Test;
+
+public class GerritConfigBuilderTest {
+
+ @Test
+ public void emptyGerritConfigContainsAllPresetConfiguration() {
+ Gerrit gerrit = createGerrit("");
+ ConfigBuilder cfgBuilder = new GerritConfigBuilder(gerrit);
+ Config cfg = cfgBuilder.build();
+ for (RequiredOption<?> opt : cfgBuilder.getRequiredOptions()) {
+ if (opt.getExpected() instanceof String || opt.getExpected() instanceof Boolean) {
+ assertTrue(
+ cfg.getString(opt.getSection(), opt.getSubSection(), opt.getKey())
+ .equals(opt.getExpected().toString()));
+ } else if (opt.getExpected() instanceof Set) {
+ assertTrue(
+ Arrays.asList(cfg.getStringList(opt.getSection(), opt.getSubSection(), opt.getKey()))
+ .containsAll((Set<?>) opt.getExpected()));
+ }
+ }
+ }
+
+ @Test
+ public void invalidConfigValueIsRejected() {
+ Gerrit gerrit = createGerrit("[gerrit]\n basePath = invalid");
+ assertThrows(IllegalStateException.class, () -> new GerritConfigBuilder(gerrit).build());
+ }
+
+ @Test
+ public void validConfigValueIsAccepted() {
+ Gerrit gerrit = createGerrit("[gerrit]\n basePath = git");
+ assertDoesNotThrow(() -> new GerritConfigBuilder(gerrit).build());
+ }
+
+ @Test
+ public void canonicalWebUrlIsConfigured() {
+ IngressConfig ingressConfig = new IngressConfig();
+ ingressConfig.setEnabled(true);
+ ingressConfig.setHost("gerrit.example.com");
+
+ GerritSpec gerritSpec = new GerritSpec();
+ gerritSpec.setIngress(ingressConfig);
+ Gerrit gerrit = new Gerrit();
+ gerrit.setSpec(gerritSpec);
+ Config cfg = new GerritConfigBuilder(gerrit).build();
+ assertTrue(
+ cfg.getString("gerrit", null, "canonicalWebUrl").equals("http://gerrit.example.com"));
+ }
+
+ private Gerrit createGerrit(String configText) {
+ GerritSpec gerritSpec = new GerritSpec();
+ gerritSpec.setConfigFiles(Map.of("gerrit.config", configText));
+ Gerrit gerrit = new Gerrit();
+ gerrit.setSpec(gerritSpec);
+ return gerrit;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollectionE2E.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollectionE2E.java
new file mode 100644
index 0000000..7f31ac2
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollectionE2E.java
@@ -0,0 +1,234 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.gitgc;
+
+import static com.google.gerrit.k8s.operator.test.TestGerritCluster.CLUSTER_NAME;
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.k8s.operator.network.IngressType;
+import com.google.gerrit.k8s.operator.test.AbstractGerritOperatorE2ETest;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gitgc.GitGarbageCollection;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gitgc.GitGarbageCollectionSpec;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gitgc.GitGarbageCollectionStatus;
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+import io.fabric8.kubernetes.api.model.batch.v1.CronJob;
+import io.fabric8.kubernetes.api.model.batch.v1.Job;
+import java.util.List;
+import java.util.Set;
+import org.junit.jupiter.api.Test;
+
+public class GitGarbageCollectionE2E extends AbstractGerritOperatorE2ETest {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ static final String GITGC_SCHEDULE = "*/1 * * * *";
+
+ @Test
+ void testGitGcAllProjectsCreationAndDeletion() {
+ GitGarbageCollection gitGc = createCompleteGc();
+
+ logger.atInfo().log("Waiting max 2 minutes for GitGc to be created.");
+ await()
+ .atMost(2, MINUTES)
+ .untilAsserted(
+ () -> {
+ assertGitGcCreation(gitGc.getMetadata().getName());
+ assertGitGcCronJobCreation(gitGc.getMetadata().getName());
+ assertGitGcJobCreation(gitGc.getMetadata().getName());
+ });
+
+ logger.atInfo().log("Deleting test GitGc object: %s", gitGc);
+ client.resource(gitGc).delete();
+ awaitGitGcDeletionAssertion(gitGc.getMetadata().getName());
+ }
+
+ @Test
+ void testGitGcSelectedProjects() {
+ GitGarbageCollection gitGc = createSelectiveGc("selective-gc", Set.of("All-Projects", "test"));
+
+ logger.atInfo().log("Waiting max 2 minutes for GitGc to be created.");
+ await()
+ .atMost(2, MINUTES)
+ .untilAsserted(
+ () -> {
+ assertGitGcCreation(gitGc.getMetadata().getName());
+ assertGitGcCronJobCreation(gitGc.getMetadata().getName());
+ assertGitGcJobCreation(gitGc.getMetadata().getName());
+ });
+
+ client.resource(gitGc).delete();
+ }
+
+ @Test
+ void testSelectiveGcIsExcludedFromCompleteGc() {
+ GitGarbageCollection completeGitGc = createCompleteGc();
+
+ logger.atInfo().log("Waiting max 2 minutes for GitGc to be created.");
+ await()
+ .atMost(2, MINUTES)
+ .untilAsserted(
+ () -> {
+ assertGitGcCreation(completeGitGc.getMetadata().getName());
+ assertGitGcCronJobCreation(completeGitGc.getMetadata().getName());
+ });
+
+ Set<String> selectedProjects = Set.of("All-Projects", "test");
+ GitGarbageCollection selectiveGitGc = createSelectiveGc("selective-gc", selectedProjects);
+
+ logger.atInfo().log("Waiting max 2 minutes for GitGc to be created.");
+ await()
+ .atMost(2, MINUTES)
+ .untilAsserted(
+ () -> {
+ assertGitGcCreation(selectiveGitGc.getMetadata().getName());
+ assertGitGcCronJobCreation(selectiveGitGc.getMetadata().getName());
+ });
+
+ await()
+ .atMost(2, MINUTES)
+ .untilAsserted(
+ () -> {
+ GitGarbageCollection updatedCompleteGitGc =
+ client
+ .resources(GitGarbageCollection.class)
+ .inNamespace(operator.getNamespace())
+ .withName(completeGitGc.getMetadata().getName())
+ .get();
+ assert updatedCompleteGitGc
+ .getStatus()
+ .getExcludedProjects()
+ .containsAll(selectedProjects);
+ });
+
+ client.resource(selectiveGitGc).delete();
+ awaitGitGcDeletionAssertion(selectiveGitGc.getMetadata().getName());
+
+ await()
+ .atMost(2, MINUTES)
+ .untilAsserted(
+ () -> {
+ GitGarbageCollection updatedCompleteGitGc =
+ client
+ .resources(GitGarbageCollection.class)
+ .inNamespace(operator.getNamespace())
+ .withName(completeGitGc.getMetadata().getName())
+ .get();
+ assert updatedCompleteGitGc.getStatus().getExcludedProjects().isEmpty();
+ });
+ }
+
+ private GitGarbageCollection createCompleteGc() {
+ GitGarbageCollection gitGc = new GitGarbageCollection();
+ gitGc.setMetadata(
+ new ObjectMetaBuilder()
+ .withName("gitgc-complete")
+ .withNamespace(operator.getNamespace())
+ .build());
+ GitGarbageCollectionSpec spec = new GitGarbageCollectionSpec();
+ spec.setSchedule(GITGC_SCHEDULE);
+ spec.setCluster(CLUSTER_NAME);
+ gitGc.setSpec(spec);
+
+ logger.atInfo().log("Creating test GitGc object: %s", gitGc);
+ client.resource(gitGc).createOrReplace();
+
+ return gitGc;
+ }
+
+ private GitGarbageCollection createSelectiveGc(String name, Set<String> projects) {
+ GitGarbageCollection gitGc = new GitGarbageCollection();
+ gitGc.setMetadata(
+ new ObjectMetaBuilder().withName(name).withNamespace(operator.getNamespace()).build());
+ GitGarbageCollectionSpec spec = new GitGarbageCollectionSpec();
+ spec.setSchedule(GITGC_SCHEDULE);
+ spec.setCluster(CLUSTER_NAME);
+ spec.setProjects(projects);
+ gitGc.setSpec(spec);
+
+ logger.atInfo().log("Creating test GitGc object: %s", gitGc);
+ client.resource(gitGc).createOrReplace();
+
+ return gitGc;
+ }
+
+ private void assertGitGcCreation(String gitGcName) {
+ GitGarbageCollection updatedGitGc =
+ client
+ .resources(GitGarbageCollection.class)
+ .inNamespace(operator.getNamespace())
+ .withName(gitGcName)
+ .get();
+ assertThat(updatedGitGc, is(notNullValue()));
+ assertThat(
+ updatedGitGc.getStatus().getState(),
+ is(not(equalTo(GitGarbageCollectionStatus.GitGcState.ERROR))));
+ }
+
+ private void assertGitGcCronJobCreation(String gitGcName) {
+ CronJob cronJob =
+ client
+ .batch()
+ .v1()
+ .cronjobs()
+ .inNamespace(operator.getNamespace())
+ .withName(gitGcName)
+ .get();
+ assertThat(cronJob, is(notNullValue()));
+ }
+
+ private void awaitGitGcDeletionAssertion(String gitGcName) {
+ logger.atInfo().log("Waiting max 2 minutes for GitGc to be deleted.");
+ await()
+ .atMost(2, MINUTES)
+ .untilAsserted(
+ () -> {
+ GitGarbageCollection updatedGitGc =
+ client
+ .resources(GitGarbageCollection.class)
+ .inNamespace(operator.getNamespace())
+ .withName(gitGcName)
+ .get();
+ assertNull(updatedGitGc);
+
+ CronJob cronJob =
+ client
+ .batch()
+ .v1()
+ .cronjobs()
+ .inNamespace(operator.getNamespace())
+ .withName(gitGcName)
+ .get();
+ assertNull(cronJob);
+ });
+ }
+
+ private void assertGitGcJobCreation(String gitGcName) {
+ List<Job> jobRuns =
+ client.batch().v1().jobs().inNamespace(operator.getNamespace()).list().getItems();
+ assert (jobRuns.size() > 0);
+ assert (jobRuns.get(0).getMetadata().getName().startsWith(gitGcName));
+ }
+
+ @Override
+ protected IngressType getIngressType() {
+ return IngressType.NONE;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterAmbassadorTest.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterAmbassadorTest.java
new file mode 100644
index 0000000..3f5a9cd
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/network/ambassador/dependent/GerritClusterAmbassadorTest.java
@@ -0,0 +1,130 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ambassador.dependent;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.getambassador.v2.Host;
+import io.getambassador.v2.Mapping;
+import io.getambassador.v2.TLSContext;
+import io.javaoperatorsdk.operator.ReconcilerUtils;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Map;
+import java.util.stream.Stream;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+public class GerritClusterAmbassadorTest {
+
+ @ParameterizedTest
+ @MethodSource("provideYamlManifests")
+ public void expectedGerritClusterAmbassadorComponentsCreated(
+ String inputFile, Map<String, String> expectedOutputFileNames)
+ throws ClassNotFoundException, NoSuchMethodException, InstantiationException,
+ IllegalAccessException, InvocationTargetException {
+ GerritNetwork gerritNetwork =
+ ReconcilerUtils.loadYaml(GerritNetwork.class, this.getClass(), inputFile);
+
+ for (Map.Entry<String, String> entry : expectedOutputFileNames.entrySet()) {
+ String className = entry.getKey();
+ String expectedOutputFile = entry.getValue();
+
+ Class<?> clazz = Class.forName(className);
+ Object dependentObject = clazz.getDeclaredConstructor(new Class[] {}).newInstance();
+
+ if (dependentObject instanceof MappingDependentResourceInterface) {
+ MappingDependentResourceInterface dependent =
+ (MappingDependentResourceInterface) dependentObject;
+ Mapping result = dependent.desired(gerritNetwork, null);
+ Mapping expected =
+ ReconcilerUtils.loadYaml(Mapping.class, this.getClass(), expectedOutputFile);
+ assertThat(result.getSpec()).isEqualTo(expected.getSpec());
+ } else if (dependentObject instanceof GerritClusterTLSContext) {
+ GerritClusterTLSContext dependent = (GerritClusterTLSContext) dependentObject;
+ TLSContext result = dependent.desired(gerritNetwork, null);
+ TLSContext expected =
+ ReconcilerUtils.loadYaml(TLSContext.class, this.getClass(), expectedOutputFile);
+ assertThat(result.getSpec()).isEqualTo(expected.getSpec());
+ } else if (dependentObject instanceof GerritClusterHost) {
+ GerritClusterHost dependent = (GerritClusterHost) dependentObject;
+ Host result = dependent.desired(gerritNetwork, null);
+ Host expected = ReconcilerUtils.loadYaml(Host.class, this.getClass(), expectedOutputFile);
+ assertThat(result.getSpec()).isEqualTo(expected.getSpec());
+ }
+ }
+ }
+
+ private static Stream<Arguments> provideYamlManifests() {
+ return Stream.of(
+ Arguments.of(
+ "../../gerritnetwork_primary_replica_tls.yaml",
+ Map.of(
+ GerritClusterMappingGETReplica.class.getName(),
+ "mappingGETReplica_primary_replica.yaml",
+ GerritClusterMappingPOSTReplica.class.getName(),
+ "mappingPOSTReplica_primary_replica.yaml",
+ GerritClusterMappingPrimary.class.getName(), "mappingPrimary_primary_replica.yaml",
+ GerritClusterTLSContext.class.getName(), "tlscontext.yaml")),
+ Arguments.of(
+ "../../gerritnetwork_primary_replica_tls_create_host.yaml",
+ Map.of(
+ GerritClusterMappingGETReplica.class.getName(),
+ "mappingGETReplica_primary_replica.yaml",
+ GerritClusterMappingPOSTReplica.class.getName(),
+ "mappingPOSTReplica_primary_replica.yaml",
+ GerritClusterMappingPrimary.class.getName(), "mappingPrimary_primary_replica.yaml",
+ GerritClusterTLSContext.class.getName(), "tlscontext.yaml",
+ GerritClusterHost.class.getName(), "host_with_tls.yaml")),
+ Arguments.of(
+ "../../gerritnetwork_primary_replica.yaml",
+ Map.of(
+ GerritClusterMappingGETReplica.class.getName(),
+ "mappingGETReplica_primary_replica.yaml",
+ GerritClusterMappingPOSTReplica.class.getName(),
+ "mappingPOSTReplica_primary_replica.yaml",
+ GerritClusterMappingPrimary.class.getName(),
+ "mappingPrimary_primary_replica.yaml")),
+ Arguments.of(
+ "../../gerritnetwork_primary_replica_create_host.yaml",
+ Map.of(
+ GerritClusterMappingGETReplica.class.getName(),
+ "mappingGETReplica_primary_replica.yaml",
+ GerritClusterMappingPOSTReplica.class.getName(),
+ "mappingPOSTReplica_primary_replica.yaml",
+ GerritClusterMappingPrimary.class.getName(), "mappingPrimary_primary_replica.yaml",
+ GerritClusterHost.class.getName(), "host.yaml")),
+ Arguments.of(
+ "../../gerritnetwork_primary.yaml",
+ Map.of(GerritClusterMapping.class.getName(), "mapping_primary.yaml")),
+ Arguments.of(
+ "../../gerritnetwork_replica.yaml",
+ Map.of(GerritClusterMapping.class.getName(), "mapping_replica.yaml")),
+ Arguments.of(
+ "../../gerritnetwork_receiver_replica.yaml",
+ Map.of(
+ GerritClusterMapping.class.getName(), "mapping_replica.yaml",
+ GerritClusterMappingReceiver.class.getName(), "mapping_receiver.yaml",
+ GerritClusterMappingReceiverGET.class.getName(), "mappingGET_receiver.yaml")),
+ Arguments.of(
+ "../../gerritnetwork_receiver_replica_tls.yaml",
+ Map.of(
+ GerritClusterMapping.class.getName(), "mapping_replica.yaml",
+ GerritClusterMappingReceiver.class.getName(), "mapping_receiver.yaml",
+ GerritClusterMappingReceiverGET.class.getName(), "mappingGET_receiver.yaml",
+ GerritClusterTLSContext.class.getName(), "tlscontext.yaml")));
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/network/ingress/dependent/GerritClusterIngressTest.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/network/ingress/dependent/GerritClusterIngressTest.java
new file mode 100644
index 0000000..a2e6a24
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/network/ingress/dependent/GerritClusterIngressTest.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.ingress.dependent;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.fabric8.kubernetes.api.model.networking.v1.Ingress;
+import io.javaoperatorsdk.operator.ReconcilerUtils;
+import java.util.stream.Stream;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+public class GerritClusterIngressTest {
+ @ParameterizedTest
+ @MethodSource("provideYamlManifests")
+ public void expectedGerritClusterIngressCreated(String inputFile, String expectedOutputFile) {
+ GerritClusterIngress dependent = new GerritClusterIngress();
+ Ingress result =
+ dependent.desired(
+ ReconcilerUtils.loadYaml(GerritNetwork.class, this.getClass(), inputFile), null);
+ Ingress expected = ReconcilerUtils.loadYaml(Ingress.class, this.getClass(), expectedOutputFile);
+ assertThat(result.getSpec()).isEqualTo(expected.getSpec());
+ assertThat(result.getMetadata().getAnnotations())
+ .containsExactlyEntriesIn(expected.getMetadata().getAnnotations());
+ }
+
+ private static Stream<Arguments> provideYamlManifests() {
+ return Stream.of(
+ Arguments.of(
+ "../../gerritnetwork_primary_replica_tls.yaml", "ingress_primary_replica_tls.yaml"),
+ Arguments.of("../../gerritnetwork_primary_replica.yaml", "ingress_primary_replica.yaml"),
+ Arguments.of("../../gerritnetwork_primary.yaml", "ingress_primary.yaml"),
+ Arguments.of("../../gerritnetwork_replica.yaml", "ingress_replica.yaml"),
+ Arguments.of("../../gerritnetwork_receiver_replica.yaml", "ingress_receiver_replica.yaml"),
+ Arguments.of(
+ "../../gerritnetwork_receiver_replica_tls.yaml", "ingress_receiver_replica_tls.yaml"));
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritClusterIstioTest.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritClusterIstioTest.java
new file mode 100644
index 0000000..eaed636
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/network/istio/dependent/GerritClusterIstioTest.java
@@ -0,0 +1,88 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.network.istio.dependent;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import io.fabric8.istio.api.networking.v1beta1.Gateway;
+import io.fabric8.istio.api.networking.v1beta1.VirtualService;
+import io.javaoperatorsdk.operator.ReconcilerUtils;
+import java.util.stream.Stream;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+public class GerritClusterIstioTest {
+ @ParameterizedTest
+ @MethodSource("provideYamlManifests")
+ public void expectedGerritClusterIstioComponentsCreated(
+ String inputFile, String expectedGatewayOutputFile, String expectedVirtualServiceOutputFile) {
+ GerritNetwork gerritNetwork =
+ ReconcilerUtils.loadYaml(GerritNetwork.class, this.getClass(), inputFile);
+ GerritClusterIstioGateway gatewayDependent = new GerritClusterIstioGateway();
+ Gateway gatewayResult = gatewayDependent.desired(gerritNetwork, null);
+ Gateway expectedGateway =
+ ReconcilerUtils.loadYaml(Gateway.class, this.getClass(), expectedGatewayOutputFile);
+ assertThat(gatewayResult.getSpec()).isEqualTo(expectedGateway.getSpec());
+
+ GerritIstioVirtualService virtualServiceDependent = new GerritIstioVirtualService();
+ VirtualService virtualServiceResult = virtualServiceDependent.desired(gerritNetwork, null);
+ VirtualService expectedVirtualService =
+ ReconcilerUtils.loadYaml(
+ VirtualService.class, this.getClass(), expectedVirtualServiceOutputFile);
+ assertThat(virtualServiceResult.getSpec()).isEqualTo(expectedVirtualService.getSpec());
+ }
+
+ private static Stream<Arguments> provideYamlManifests() {
+ return Stream.of(
+ Arguments.of(
+ "../../gerritnetwork_primary_replica_tls.yaml",
+ "gateway_tls.yaml",
+ "virtualservice_primary_replica.yaml"),
+ Arguments.of(
+ "../../gerritnetwork_primary_replica.yaml",
+ "gateway.yaml",
+ "virtualservice_primary_replica.yaml"),
+ Arguments.of(
+ "../../gerritnetwork_primary.yaml", "gateway.yaml", "virtualservice_primary.yaml"),
+ Arguments.of(
+ "../../gerritnetwork_replica.yaml", "gateway.yaml", "virtualservice_replica.yaml"),
+ Arguments.of(
+ "../../gerritnetwork_receiver_replica.yaml",
+ "gateway.yaml",
+ "virtualservice_receiver_replica.yaml"),
+ Arguments.of(
+ "../../gerritnetwork_receiver_replica_tls.yaml",
+ "gateway_tls.yaml",
+ "virtualservice_receiver_replica.yaml"),
+ Arguments.of(
+ "../../gerritnetwork_primary_ssh.yaml",
+ "gateway_primary_ssh.yaml",
+ "virtualservice_primary_ssh.yaml"),
+ Arguments.of(
+ "../../gerritnetwork_replica_ssh.yaml",
+ "gateway_replica_ssh.yaml",
+ "virtualservice_replica_ssh.yaml"),
+ Arguments.of(
+ "../../gerritnetwork_primary_replica_ssh.yaml",
+ "gateway_primary_replica_ssh.yaml",
+ "virtualservice_primary_replica_ssh.yaml"),
+ Arguments.of(
+ "../../gerritnetwork_receiver_replica_ssh.yaml",
+ "gateway_receiver_replica_ssh.yaml",
+ "virtualservice_receiver_replica_ssh.yaml"));
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/AbstractClusterManagedReceiverE2E.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/AbstractClusterManagedReceiverE2E.java
new file mode 100644
index 0000000..557e140
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/AbstractClusterManagedReceiverE2E.java
@@ -0,0 +1,110 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.receiver;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.gerrit.k8s.operator.test.AbstractGerritOperatorE2ETest;
+import com.google.gerrit.k8s.operator.test.ReceiverUtil;
+import com.google.gerrit.k8s.operator.test.TestGerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplate;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplateSpec.GerritMode;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.ReceiverTemplate;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.ReceiverTemplateSpec;
+import io.fabric8.kubernetes.api.model.ObjectMeta;
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+import java.io.File;
+import java.net.URL;
+import java.nio.file.Path;
+import org.apache.http.client.utils.URIBuilder;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.RefSpec;
+import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+public abstract class AbstractClusterManagedReceiverE2E extends AbstractGerritOperatorE2ETest {
+ private static final String GERRIT_NAME = "gerrit";
+ private ReceiverTemplate receiver;
+ private GerritTemplate gerrit;
+
+ @BeforeEach
+ public void setupComponents() throws Exception {
+ gerrit = TestGerrit.createGerritTemplate(GERRIT_NAME, GerritMode.REPLICA);
+ gerritCluster.addGerrit(gerrit);
+
+ receiver = new ReceiverTemplate();
+ ObjectMeta receiverMeta = new ObjectMetaBuilder().withName("receiver").build();
+ receiver.setMetadata(receiverMeta);
+ ReceiverTemplateSpec receiverTemplateSpec = new ReceiverTemplateSpec();
+ receiverTemplateSpec.setReplicas(2);
+ receiverTemplateSpec.setCredentialSecretRef(ReceiverUtil.CREDENTIALS_SECRET_NAME);
+ receiver.setSpec(receiverTemplateSpec);
+ gerritCluster.setReceiver(receiver);
+ gerritCluster.deploy();
+ }
+
+ @Test
+ public void testProjectLifecycle(@TempDir Path tempDir) throws Exception {
+ GerritCluster cluster = gerritCluster.getGerritCluster();
+ assertProjectLifecycle(cluster, tempDir);
+ }
+
+ private void assertProjectLifecycle(GerritCluster cluster, Path tempDir) throws Exception {
+ assertThat(
+ ReceiverUtil.sendReceiverApiRequest(cluster, "PUT", "/a/projects/test.git"),
+ is(equalTo(201)));
+ CredentialsProvider gerritCredentials =
+ new UsernamePasswordCredentialsProvider(
+ testProps.getGerritUser(), testProps.getGerritPwd());
+ Git git =
+ Git.cloneRepository()
+ .setURI(getGerritUrl("/test.git").toString())
+ .setCredentialsProvider(gerritCredentials)
+ .setDirectory(tempDir.toFile())
+ .call();
+ new File("test.txt").createNewFile();
+ git.add().addFilepattern(".").call();
+ RevCommit commit = git.commit().setMessage("test commit").call();
+ git.push()
+ .setCredentialsProvider(
+ new UsernamePasswordCredentialsProvider(
+ ReceiverUtil.RECEIVER_TEST_USER, ReceiverUtil.RECEIVER_TEST_PASSWORD))
+ .setRefSpecs(new RefSpec("refs/heads/master"))
+ .call();
+ assertTrue(
+ git.lsRemote().setCredentialsProvider(gerritCredentials).setRemote("origin").call().stream()
+ .anyMatch(ref -> ref.getObjectId().equals(commit.getId())));
+ assertThat(
+ ReceiverUtil.sendReceiverApiRequest(cluster, "DELETE", "/a/projects/test.git"),
+ is(equalTo(204)));
+ }
+
+ private URL getGerritUrl(String path) throws Exception {
+ return new URIBuilder()
+ .setScheme("https")
+ .setHost(gerritCluster.getHostname())
+ .setPath(path)
+ .build()
+ .toURL();
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/ClusterManagedReceiverWithIngressE2E.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/ClusterManagedReceiverWithIngressE2E.java
new file mode 100644
index 0000000..4dd8bd3
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/ClusterManagedReceiverWithIngressE2E.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.receiver;
+
+import com.google.gerrit.k8s.operator.network.IngressType;
+
+public class ClusterManagedReceiverWithIngressE2E extends AbstractClusterManagedReceiverE2E {
+
+ @Override
+ protected IngressType getIngressType() {
+ return IngressType.INGRESS;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/ClusterManagedReceiverWithIstioE2E.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/ClusterManagedReceiverWithIstioE2E.java
new file mode 100644
index 0000000..e76303c
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/ClusterManagedReceiverWithIstioE2E.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.receiver;
+
+import com.google.gerrit.k8s.operator.network.IngressType;
+
+public class ClusterManagedReceiverWithIstioE2E extends AbstractClusterManagedReceiverE2E {
+ @Override
+ protected IngressType getIngressType() {
+ return IngressType.ISTIO;
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/dependent/ReceiverTest.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/dependent/ReceiverTest.java
new file mode 100644
index 0000000..8698b1a
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/dependent/ReceiverTest.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.receiver.dependent;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.Receiver;
+import io.fabric8.kubernetes.api.model.Service;
+import io.fabric8.kubernetes.api.model.apps.Deployment;
+import io.javaoperatorsdk.operator.ReconcilerUtils;
+import java.util.stream.Stream;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+public class ReceiverTest {
+ @ParameterizedTest
+ @MethodSource("provideYamlManifests")
+ public void expectedReceiverComponentsCreated(
+ String inputFile, String expectedDeployment, String expectedService) {
+ Receiver input = ReconcilerUtils.loadYaml(Receiver.class, this.getClass(), inputFile);
+ ReceiverDeployment dependentDeployment = new ReceiverDeployment();
+ assertThat(dependentDeployment.desired(input, null))
+ .isEqualTo(ReconcilerUtils.loadYaml(Deployment.class, this.getClass(), expectedDeployment));
+
+ ReceiverService dependentService = new ReceiverService();
+ assertThat(dependentService.desired(input, null))
+ .isEqualTo(ReconcilerUtils.loadYaml(Service.class, this.getClass(), expectedService));
+ }
+
+ private static Stream<Arguments> provideYamlManifests() {
+ return Stream.of(
+ Arguments.of("../receiver.yaml", "deployment.yaml", "service.yaml"),
+ Arguments.of("../receiver_minimal.yaml", "deployment_minimal.yaml", "service.yaml"));
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/server/GerritAdmissionWebhookTest.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/server/GerritAdmissionWebhookTest.java
new file mode 100644
index 0000000..32b342d
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/server/GerritAdmissionWebhookTest.java
@@ -0,0 +1,182 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.server;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.gerrit.k8s.operator.test.TestAdmissionWebhookServer;
+import com.google.gerrit.k8s.operator.v1alpha.admission.servlet.GerritAdmissionWebhook;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritClusterSpec;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritSpec;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplateSpec.GerritMode;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.Receiver;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GerritClusterIngressConfig;
+import io.fabric8.kubernetes.api.model.DefaultKubernetesResourceList;
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.fabric8.kubernetes.api.model.ObjectMeta;
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+import io.fabric8.kubernetes.api.model.admission.v1.AdmissionRequest;
+import io.fabric8.kubernetes.api.model.admission.v1.AdmissionReview;
+import io.fabric8.kubernetes.client.server.mock.KubernetesServer;
+import io.fabric8.kubernetes.internal.KubernetesDeserializer;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Map;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Rule;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestInstance.Lifecycle;
+
+@TestInstance(Lifecycle.PER_CLASS)
+public class GerritAdmissionWebhookTest {
+ private static final String NAMESPACE = "test";
+ private static final String LIST_GERRITS_PATH =
+ String.format(
+ "/apis/%s/namespaces/%s/%s",
+ HasMetadata.getApiVersion(Gerrit.class), NAMESPACE, HasMetadata.getPlural(Gerrit.class));
+ private static final String LIST_GERRIT_CLUSTERS_PATH =
+ String.format(
+ "/apis/%s/namespaces/%s/%s",
+ HasMetadata.getApiVersion(GerritCluster.class),
+ NAMESPACE,
+ HasMetadata.getPlural(GerritCluster.class));
+ private TestAdmissionWebhookServer server;
+
+ @Rule public KubernetesServer kubernetesServer = new KubernetesServer();
+
+ @BeforeAll
+ public void setup() throws Exception {
+ KubernetesDeserializer.registerCustomKind(
+ "gerritoperator.google.com/v1alpha2", "Gerrit", Gerrit.class);
+ KubernetesDeserializer.registerCustomKind(
+ "gerritoperator.google.com/v1alpha1", "Receiver", Receiver.class);
+ server = new TestAdmissionWebhookServer();
+
+ kubernetesServer.before();
+
+ GerritAdmissionWebhook webhook = new GerritAdmissionWebhook();
+ server.registerWebhook(webhook);
+ server.start();
+ }
+
+ @Test
+ public void testInvalidGerritConfigRejected() throws Exception {
+ String clusterName = "gerrit";
+ Config gerritConfig = new Config();
+ gerritConfig.setString("container", null, "user", "gerrit");
+ Gerrit gerrit = createGerrit(clusterName, gerritConfig);
+ kubernetesServer
+ .expect()
+ .get()
+ .withPath(LIST_GERRITS_PATH)
+ .andReturn(HttpURLConnection.HTTP_OK, new DefaultKubernetesResourceList<Gerrit>())
+ .times(2);
+
+ mockGerritCluster(clusterName);
+
+ HttpURLConnection http = sendAdmissionRequest(gerrit);
+
+ AdmissionReview response =
+ new ObjectMapper().readValue(http.getInputStream(), AdmissionReview.class);
+
+ assertThat(http.getResponseCode(), is(equalTo(HttpServletResponse.SC_OK)));
+ assertThat(response.getResponse().getAllowed(), is(true));
+
+ gerritConfig.setString("container", null, "user", "invalid");
+ Gerrit gerrit2 = createGerrit(clusterName, gerritConfig);
+ HttpURLConnection http2 = sendAdmissionRequest(gerrit2);
+
+ AdmissionReview response2 =
+ new ObjectMapper().readValue(http2.getInputStream(), AdmissionReview.class);
+
+ assertThat(http2.getResponseCode(), is(equalTo(HttpServletResponse.SC_OK)));
+ assertThat(response2.getResponse().getAllowed(), is(false));
+ }
+
+ private void mockGerritCluster(String name) {
+ GerritCluster cluster = new GerritCluster();
+ cluster.setMetadata(new ObjectMetaBuilder().withName(name).withNamespace(NAMESPACE).build());
+ GerritClusterSpec clusterSpec = new GerritClusterSpec();
+ GerritClusterIngressConfig ingressConfig = new GerritClusterIngressConfig();
+ ingressConfig.setEnabled(false);
+ clusterSpec.setIngress(ingressConfig);
+ clusterSpec.setServerId("test");
+ cluster.setSpec(clusterSpec);
+
+ kubernetesServer
+ .expect()
+ .get()
+ .withPath(LIST_GERRIT_CLUSTERS_PATH + "/" + name)
+ .andReturn(HttpURLConnection.HTTP_OK, cluster)
+ .always();
+ }
+
+ private Gerrit createGerrit(String cluster, Config gerritConfig) {
+ ObjectMeta meta =
+ new ObjectMetaBuilder()
+ .withName(RandomStringUtils.random(10))
+ .withNamespace(NAMESPACE)
+ .build();
+ GerritSpec gerritSpec = new GerritSpec();
+ gerritSpec.setMode(GerritMode.PRIMARY);
+ if (gerritConfig != null) {
+ gerritSpec.setConfigFiles(Map.of("gerrit.config", gerritConfig.toText()));
+ }
+ Gerrit gerrit = new Gerrit();
+ gerrit.setMetadata(meta);
+ gerrit.setSpec(gerritSpec);
+ return gerrit;
+ }
+
+ private HttpURLConnection sendAdmissionRequest(Gerrit gerrit)
+ throws MalformedURLException, IOException {
+ HttpURLConnection http =
+ (HttpURLConnection)
+ new URL("http://localhost:8080/admission/v1alpha/gerrit").openConnection();
+ http.setRequestMethod(HttpMethod.POST.asString());
+ http.setRequestProperty("Content-Type", "application/json");
+ http.setDoOutput(true);
+
+ AdmissionRequest admissionReq = new AdmissionRequest();
+ admissionReq.setObject(gerrit);
+ AdmissionReview admissionReview = new AdmissionReview();
+ admissionReview.setRequest(admissionReq);
+
+ try (OutputStream os = http.getOutputStream()) {
+ byte[] input = new ObjectMapper().writer().writeValueAsBytes(admissionReview);
+ os.write(input, 0, input.length);
+ }
+ return http;
+ }
+
+ @AfterAll
+ public void shutdown() throws Exception {
+ server.stop();
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/server/GerritClusterAdmissionWebhookTest.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/server/GerritClusterAdmissionWebhookTest.java
new file mode 100644
index 0000000..55526c7
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/server/GerritClusterAdmissionWebhookTest.java
@@ -0,0 +1,195 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.server;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.gerrit.k8s.operator.test.ReceiverUtil;
+import com.google.gerrit.k8s.operator.test.TestAdmissionWebhookServer;
+import com.google.gerrit.k8s.operator.test.TestGerrit;
+import com.google.gerrit.k8s.operator.test.TestGerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.admission.servlet.GerritAdmissionWebhook;
+import com.google.gerrit.k8s.operator.v1alpha.admission.servlet.GerritClusterAdmissionWebhook;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplate;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplateSpec.GerritMode;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.Receiver;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.ReceiverTemplate;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.ReceiverTemplateSpec;
+import io.fabric8.kubernetes.api.model.ObjectMeta;
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+import io.fabric8.kubernetes.api.model.admission.v1.AdmissionRequest;
+import io.fabric8.kubernetes.api.model.admission.v1.AdmissionReview;
+import io.fabric8.kubernetes.client.server.mock.KubernetesServer;
+import io.fabric8.kubernetes.internal.KubernetesDeserializer;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Rule;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestInstance.Lifecycle;
+
+@TestInstance(Lifecycle.PER_CLASS)
+public class GerritClusterAdmissionWebhookTest {
+ private static final String NAMESPACE = "test";
+ private TestAdmissionWebhookServer server;
+
+ @Rule public KubernetesServer kubernetesServer = new KubernetesServer();
+
+ @BeforeAll
+ public void setup() throws Exception {
+ KubernetesDeserializer.registerCustomKind(
+ "gerritoperator.google.com/v1alpha2", "Gerrit", Gerrit.class);
+ KubernetesDeserializer.registerCustomKind(
+ "gerritoperator.google.com/v1alpha2", "GerritCluster", GerritCluster.class);
+ KubernetesDeserializer.registerCustomKind(
+ "gerritoperator.google.com/v1alpha2", "Receiver", Receiver.class);
+ server = new TestAdmissionWebhookServer();
+
+ kubernetesServer.before();
+
+ server.registerWebhook(new GerritClusterAdmissionWebhook());
+ server.registerWebhook(new GerritAdmissionWebhook());
+ server.start();
+ }
+
+ @Test
+ public void testOnlySinglePrimaryGerritIsAcceptedPerGerritCluster() throws Exception {
+ Config cfg = new Config();
+ cfg.fromText(TestGerrit.DEFAULT_GERRIT_CONFIG);
+ GerritTemplate gerrit1 = TestGerrit.createGerritTemplate("gerrit1", GerritMode.PRIMARY, cfg);
+ TestGerritCluster gerritCluster =
+ new TestGerritCluster(kubernetesServer.getClient(), NAMESPACE);
+ gerritCluster.addGerrit(gerrit1);
+ GerritCluster cluster = gerritCluster.build();
+
+ HttpURLConnection http = sendAdmissionRequest(cluster);
+
+ AdmissionReview response =
+ new ObjectMapper().readValue(http.getInputStream(), AdmissionReview.class);
+
+ assertThat(http.getResponseCode(), is(equalTo(HttpServletResponse.SC_OK)));
+ assertThat(response.getResponse().getAllowed(), is(true));
+
+ GerritTemplate gerrit2 = TestGerrit.createGerritTemplate("gerrit2", GerritMode.PRIMARY, cfg);
+ gerritCluster.addGerrit(gerrit2);
+ HttpURLConnection http2 = sendAdmissionRequest(gerritCluster.build());
+
+ AdmissionReview response2 =
+ new ObjectMapper().readValue(http2.getInputStream(), AdmissionReview.class);
+
+ assertThat(http2.getResponseCode(), is(equalTo(HttpServletResponse.SC_OK)));
+ assertThat(response2.getResponse().getAllowed(), is(false));
+ assertThat(
+ response2.getResponse().getStatus().getCode(),
+ is(equalTo(HttpServletResponse.SC_CONFLICT)));
+ }
+
+ @Test
+ public void testPrimaryGerritAndReceiverAreNotAcceptedInSameGerritCluster() throws Exception {
+ Config cfg = new Config();
+ cfg.fromText(TestGerrit.DEFAULT_GERRIT_CONFIG);
+ GerritTemplate gerrit = TestGerrit.createGerritTemplate("gerrit1", GerritMode.PRIMARY, cfg);
+ TestGerritCluster gerritCluster =
+ new TestGerritCluster(kubernetesServer.getClient(), NAMESPACE);
+ gerritCluster.addGerrit(gerrit);
+
+ ReceiverTemplate receiver = new ReceiverTemplate();
+ ObjectMeta receiverMeta = new ObjectMetaBuilder().withName("receiver").build();
+ receiver.setMetadata(receiverMeta);
+ ReceiverTemplateSpec receiverTemplateSpec = new ReceiverTemplateSpec();
+ receiverTemplateSpec.setReplicas(2);
+ receiverTemplateSpec.setCredentialSecretRef(ReceiverUtil.CREDENTIALS_SECRET_NAME);
+ receiver.setSpec(receiverTemplateSpec);
+
+ gerritCluster.setReceiver(receiver);
+ HttpURLConnection http2 = sendAdmissionRequest(gerritCluster.build());
+
+ AdmissionReview response2 =
+ new ObjectMapper().readValue(http2.getInputStream(), AdmissionReview.class);
+
+ assertThat(http2.getResponseCode(), is(equalTo(HttpServletResponse.SC_OK)));
+ assertThat(response2.getResponse().getAllowed(), is(false));
+ assertThat(
+ response2.getResponse().getStatus().getCode(),
+ is(equalTo(HttpServletResponse.SC_CONFLICT)));
+ }
+
+ @Test
+ public void testPrimaryAndReplicaAreAcceptedInSameGerritCluster() throws Exception {
+ Config cfg = new Config();
+ cfg.fromText(TestGerrit.DEFAULT_GERRIT_CONFIG);
+ GerritTemplate gerrit1 = TestGerrit.createGerritTemplate("gerrit1", GerritMode.PRIMARY, cfg);
+ TestGerritCluster gerritCluster =
+ new TestGerritCluster(kubernetesServer.getClient(), NAMESPACE);
+ gerritCluster.addGerrit(gerrit1);
+
+ HttpURLConnection http = sendAdmissionRequest(gerritCluster.build());
+
+ AdmissionReview response =
+ new ObjectMapper().readValue(http.getInputStream(), AdmissionReview.class);
+
+ assertThat(http.getResponseCode(), is(equalTo(HttpServletResponse.SC_OK)));
+ assertThat(response.getResponse().getAllowed(), is(true));
+
+ GerritTemplate gerrit2 = TestGerrit.createGerritTemplate("gerrit2", GerritMode.REPLICA, cfg);
+ gerritCluster.addGerrit(gerrit2);
+ HttpURLConnection http2 = sendAdmissionRequest(gerritCluster.build());
+
+ AdmissionReview response2 =
+ new ObjectMapper().readValue(http2.getInputStream(), AdmissionReview.class);
+
+ assertThat(http.getResponseCode(), is(equalTo(HttpServletResponse.SC_OK)));
+ assertThat(response2.getResponse().getAllowed(), is(true));
+ }
+
+ private HttpURLConnection sendAdmissionRequest(GerritCluster gerritCluster)
+ throws MalformedURLException, IOException {
+ HttpURLConnection http =
+ (HttpURLConnection)
+ new URL("http://localhost:8080/admission/v1alpha/gerritcluster").openConnection();
+ http.setRequestMethod(HttpMethod.POST.asString());
+ http.setRequestProperty("Content-Type", "application/json");
+ http.setDoOutput(true);
+
+ AdmissionRequest admissionReq = new AdmissionRequest();
+ admissionReq.setObject(gerritCluster);
+ AdmissionReview admissionReview = new AdmissionReview();
+ admissionReview.setRequest(admissionReq);
+
+ try (OutputStream os = http.getOutputStream()) {
+ byte[] input = new ObjectMapper().writer().writeValueAsBytes(admissionReview);
+ os.write(input, 0, input.length);
+ }
+ return http;
+ }
+
+ @AfterAll
+ public void shutdown() throws Exception {
+ server.stop();
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/server/GitGcAdmissionWebhookTest.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/server/GitGcAdmissionWebhookTest.java
new file mode 100644
index 0000000..5553e95
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/server/GitGcAdmissionWebhookTest.java
@@ -0,0 +1,256 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.server;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.gerrit.k8s.operator.test.TestAdmissionWebhookServer;
+import com.google.gerrit.k8s.operator.v1alpha.admission.servlet.GitGcAdmissionWebhook;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gitgc.GitGarbageCollection;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gitgc.GitGarbageCollectionSpec;
+import io.fabric8.kubernetes.api.model.DefaultKubernetesResourceList;
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+import io.fabric8.kubernetes.api.model.admission.v1.AdmissionRequest;
+import io.fabric8.kubernetes.api.model.admission.v1.AdmissionReview;
+import io.fabric8.kubernetes.client.server.mock.KubernetesServer;
+import io.fabric8.kubernetes.internal.KubernetesDeserializer;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.List;
+import java.util.Set;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.eclipse.jetty.http.HttpMethod;
+import org.junit.Rule;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestInstance.Lifecycle;
+
+@TestInstance(Lifecycle.PER_CLASS)
+public class GitGcAdmissionWebhookTest {
+ private static final String NAMESPACE = "test";
+ private static final String LIST_GITGCS_PATH =
+ String.format(
+ "/apis/%s/namespaces/%s/%s",
+ HasMetadata.getApiVersion(GitGarbageCollection.class),
+ NAMESPACE,
+ HasMetadata.getPlural(GitGarbageCollection.class));
+ private TestAdmissionWebhookServer server;
+
+ @Rule public KubernetesServer kubernetesServer = new KubernetesServer();
+
+ @BeforeAll
+ public void setup() throws Exception {
+ KubernetesDeserializer.registerCustomKind(
+ "gerritoperator.google.com/v1alpha16", "GerritCluster", GerritCluster.class);
+ KubernetesDeserializer.registerCustomKind(
+ "gerritoperator.google.com/v1alpha1", "GitGarbageCollection", GitGarbageCollection.class);
+ server = new TestAdmissionWebhookServer();
+
+ kubernetesServer.before();
+
+ GitGcAdmissionWebhook webhook = new GitGcAdmissionWebhook(kubernetesServer.getClient());
+ server.registerWebhook(webhook);
+ server.start();
+ }
+
+ @Test
+ @DisplayName("Only a single GitGC that works on all projects in site is allowed.")
+ public void testOnlySingleGitGcWorkingOnAllProjectsIsAllowed() throws Exception {
+ GitGarbageCollection gitGc = createCompleteGitGc();
+ kubernetesServer
+ .expect()
+ .get()
+ .withPath(LIST_GITGCS_PATH)
+ .andReturn(
+ HttpURLConnection.HTTP_OK, new DefaultKubernetesResourceList<GitGarbageCollection>())
+ .once();
+
+ HttpURLConnection http = sendAdmissionRequest(gitGc);
+
+ AdmissionReview response =
+ new ObjectMapper().readValue(http.getInputStream(), AdmissionReview.class);
+
+ assertThat(http.getResponseCode(), is(equalTo(HttpServletResponse.SC_OK)));
+ assertThat(response.getResponse().getAllowed(), is(true));
+
+ DefaultKubernetesResourceList<GitGarbageCollection> existingGitGcs =
+ new DefaultKubernetesResourceList<GitGarbageCollection>();
+ existingGitGcs.setItems(List.of(createCompleteGitGc()));
+ kubernetesServer
+ .expect()
+ .get()
+ .withPath(LIST_GITGCS_PATH)
+ .andReturn(HttpURLConnection.HTTP_OK, existingGitGcs)
+ .once();
+
+ HttpURLConnection http2 = sendAdmissionRequest(gitGc);
+
+ AdmissionReview response2 =
+ new ObjectMapper().readValue(http2.getInputStream(), AdmissionReview.class);
+
+ assertThat(http2.getResponseCode(), is(equalTo(HttpServletResponse.SC_OK)));
+ assertThat(response2.getResponse().getAllowed(), is(false));
+ assertThat(
+ response2.getResponse().getStatus().getCode(),
+ is(equalTo(HttpServletResponse.SC_CONFLICT)));
+ }
+
+ @Test
+ @DisplayName(
+ "A GitGc configured to work on all projects and selective GitGcs are allowed to exist at the same time.")
+ public void testSelectiveAndCompleteGitGcAreAllowedTogether() throws Exception {
+ DefaultKubernetesResourceList<GitGarbageCollection> existingGitGcs =
+ new DefaultKubernetesResourceList<GitGarbageCollection>();
+ existingGitGcs.setItems(List.of(createCompleteGitGc()));
+ kubernetesServer
+ .expect()
+ .get()
+ .withPath(LIST_GITGCS_PATH)
+ .andReturn(HttpURLConnection.HTTP_OK, existingGitGcs)
+ .once();
+
+ GitGarbageCollection gitGc2 = createGitGcForProjects(Set.of("project3"));
+ HttpURLConnection http2 = sendAdmissionRequest(gitGc2);
+
+ AdmissionReview response2 =
+ new ObjectMapper().readValue(http2.getInputStream(), AdmissionReview.class);
+
+ assertThat(http2.getResponseCode(), is(equalTo(HttpServletResponse.SC_OK)));
+ assertThat(response2.getResponse().getAllowed(), is(true));
+ }
+
+ @Test
+ @DisplayName("Multiple selectve GitGcs working on a different set of projects are allowed.")
+ public void testNonConflictingSelectiveGcsAreAllowed() throws Exception {
+ GitGarbageCollection gitGc = createGitGcForProjects(Set.of("project1", "project2"));
+ DefaultKubernetesResourceList<GitGarbageCollection> existingGitGcs =
+ new DefaultKubernetesResourceList<GitGarbageCollection>();
+ existingGitGcs.setItems(List.of(gitGc));
+ kubernetesServer
+ .expect()
+ .get()
+ .withPath(LIST_GITGCS_PATH)
+ .andReturn(HttpURLConnection.HTTP_OK, existingGitGcs)
+ .once();
+
+ GitGarbageCollection gitGc2 = createGitGcForProjects(Set.of("project3"));
+ HttpURLConnection http2 = sendAdmissionRequest(gitGc2);
+
+ AdmissionReview response2 =
+ new ObjectMapper().readValue(http2.getInputStream(), AdmissionReview.class);
+
+ assertThat(http2.getResponseCode(), is(equalTo(HttpServletResponse.SC_OK)));
+ assertThat(response2.getResponse().getAllowed(), is(true));
+ }
+
+ @Test
+ @DisplayName("Multiple selectve GitGcs working on the same project(s) are not allowed.")
+ public void testConflictingSelectiveGcsNotAllowed() throws Exception {
+ GitGarbageCollection gitGc = createGitGcForProjects(Set.of("project1", "project2"));
+ kubernetesServer
+ .expect()
+ .get()
+ .withPath(LIST_GITGCS_PATH)
+ .andReturn(
+ HttpURLConnection.HTTP_OK, new DefaultKubernetesResourceList<GitGarbageCollection>())
+ .once();
+
+ HttpURLConnection http = sendAdmissionRequest(gitGc);
+
+ AdmissionReview response =
+ new ObjectMapper().readValue(http.getInputStream(), AdmissionReview.class);
+
+ assertThat(http.getResponseCode(), is(equalTo(HttpServletResponse.SC_OK)));
+ assertThat(response.getResponse().getAllowed(), is(true));
+
+ DefaultKubernetesResourceList<GitGarbageCollection> existingGitGcs =
+ new DefaultKubernetesResourceList<GitGarbageCollection>();
+ existingGitGcs.setItems(List.of(gitGc));
+ kubernetesServer
+ .expect()
+ .get()
+ .withPath(LIST_GITGCS_PATH)
+ .andReturn(HttpURLConnection.HTTP_OK, existingGitGcs)
+ .once();
+
+ GitGarbageCollection gitGc2 = createGitGcForProjects(Set.of("project1"));
+ HttpURLConnection http2 = sendAdmissionRequest(gitGc2);
+
+ AdmissionReview response2 =
+ new ObjectMapper().readValue(http2.getInputStream(), AdmissionReview.class);
+
+ assertThat(http2.getResponseCode(), is(equalTo(HttpServletResponse.SC_OK)));
+ assertThat(response2.getResponse().getAllowed(), is(false));
+ assertThat(
+ response2.getResponse().getStatus().getCode(),
+ is(equalTo(HttpServletResponse.SC_CONFLICT)));
+ }
+
+ private GitGarbageCollection createCompleteGitGc() {
+ return createGitGcForProjects(Set.of());
+ }
+
+ private GitGarbageCollection createGitGcForProjects(Set<String> projects) {
+ GitGarbageCollectionSpec spec = new GitGarbageCollectionSpec();
+ spec.setProjects(projects);
+ GitGarbageCollection gitGc = new GitGarbageCollection();
+ gitGc.setMetadata(
+ new ObjectMetaBuilder()
+ .withName(RandomStringUtils.randomAlphabetic(10))
+ .withUid(RandomStringUtils.randomAlphabetic(10))
+ .withNamespace(NAMESPACE)
+ .build());
+ gitGc.setSpec(spec);
+ return gitGc;
+ }
+
+ private HttpURLConnection sendAdmissionRequest(GitGarbageCollection gitGc)
+ throws MalformedURLException, IOException {
+ HttpURLConnection http =
+ (HttpURLConnection)
+ new URL("http://localhost:8080/admission/v1alpha/gitgc").openConnection();
+ http.setRequestMethod(HttpMethod.POST.asString());
+ http.setRequestProperty("Content-Type", "application/json");
+ http.setDoOutput(true);
+
+ AdmissionRequest admissionReq = new AdmissionRequest();
+ admissionReq.setObject(gitGc);
+ AdmissionReview admissionReview = new AdmissionReview();
+ admissionReview.setRequest(admissionReq);
+
+ try (OutputStream os = http.getOutputStream()) {
+ byte[] input = new ObjectMapper().writer().writeValueAsBytes(admissionReview);
+ os.write(input, 0, input.length);
+ }
+ return http;
+ }
+
+ @AfterAll
+ public void shutdown() throws Exception {
+ server.stop();
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/AbstractGerritOperatorE2ETest.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/AbstractGerritOperatorE2ETest.java
new file mode 100644
index 0000000..ee1aa01
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/AbstractGerritOperatorE2ETest.java
@@ -0,0 +1,140 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.test;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.k8s.operator.cluster.GerritClusterReconciler;
+import com.google.gerrit.k8s.operator.gerrit.GerritReconciler;
+import com.google.gerrit.k8s.operator.gitgc.GitGarbageCollectionReconciler;
+import com.google.gerrit.k8s.operator.network.GerritNetworkReconcilerProvider;
+import com.google.gerrit.k8s.operator.network.IngressType;
+import com.google.gerrit.k8s.operator.receiver.ReceiverReconciler;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gitgc.GitGarbageCollection;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.network.GerritNetwork;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.Receiver;
+import io.fabric8.kubernetes.api.model.Secret;
+import io.fabric8.kubernetes.api.model.SecretBuilder;
+import io.fabric8.kubernetes.client.Config;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import io.fabric8.kubernetes.client.KubernetesClientBuilder;
+import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
+import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Base64;
+import java.util.Map;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mockito.Mockito;
+
+public abstract class AbstractGerritOperatorE2ETest {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ protected static final KubernetesClient client = getKubernetesClient();
+ public static final String IMAGE_PULL_SECRET_NAME = "image-pull-secret";
+ public static final TestProperties testProps = new TestProperties();
+
+ protected GerritReconciler gerritReconciler = Mockito.spy(new GerritReconciler(client));
+ protected TestGerritCluster gerritCluster;
+ protected TestSecureConfig secureConfig;
+ protected Secret receiverCredentials;
+
+ @RegisterExtension
+ protected LocallyRunOperatorExtension operator =
+ LocallyRunOperatorExtension.builder()
+ .withNamespaceDeleteTimeout(120)
+ .waitForNamespaceDeletion(true)
+ .withReconciler(new GerritClusterReconciler())
+ .withReconciler(gerritReconciler)
+ .withReconciler(new GitGarbageCollectionReconciler(client))
+ .withReconciler(new ReceiverReconciler(client))
+ .withReconciler(getGerritNetworkReconciler())
+ .build();
+
+ @BeforeEach
+ void setup() {
+ Mockito.reset(gerritReconciler);
+ createImagePullSecret(client, operator.getNamespace());
+
+ secureConfig = new TestSecureConfig(client, testProps, operator.getNamespace());
+ secureConfig.createOrReplace();
+
+ receiverCredentials = ReceiverUtil.createCredentialsSecret(operator.getNamespace());
+
+ client.resource(receiverCredentials).inNamespace(operator.getNamespace()).createOrReplace();
+
+ gerritCluster = new TestGerritCluster(client, operator.getNamespace());
+ gerritCluster.setIngressType(getIngressType());
+ gerritCluster.deploy();
+ }
+
+ @AfterEach
+ void cleanup() {
+ client.resources(Gerrit.class).inNamespace(operator.getNamespace()).delete();
+ client.resources(Receiver.class).inNamespace(operator.getNamespace()).delete();
+ client.resources(GitGarbageCollection.class).inNamespace(operator.getNamespace()).delete();
+ client.resources(GerritCluster.class).inNamespace(operator.getNamespace()).delete();
+ client.resource(receiverCredentials).inNamespace(operator.getNamespace()).delete();
+ }
+
+ private static KubernetesClient getKubernetesClient() {
+ Config config;
+ try {
+ String kubeconfig = System.getenv("KUBECONFIG");
+ if (kubeconfig != null) {
+ config = Config.fromKubeconfig(Files.readString(Path.of(kubeconfig)));
+ return new KubernetesClientBuilder().withConfig(config).build();
+ }
+ logger.atWarning().log("KUBECONFIG variable not set. Using default config.");
+ } catch (IOException e) {
+ logger.atSevere().log("Failed to load kubeconfig. Trying default", e);
+ }
+ return new KubernetesClientBuilder().build();
+ }
+
+ private static void createImagePullSecret(KubernetesClient client, String namespace) {
+ StringBuilder secretBuilder = new StringBuilder();
+ secretBuilder.append("{\"auths\": {\"");
+ secretBuilder.append(testProps.getRegistry());
+ secretBuilder.append("\": {\"auth\": \"");
+ secretBuilder.append(
+ Base64.getEncoder()
+ .encodeToString(
+ String.format("%s:%s", testProps.getRegistryUser(), testProps.getRegistryPwd())
+ .getBytes()));
+ secretBuilder.append("\"}}}");
+ String data = Base64.getEncoder().encodeToString(secretBuilder.toString().getBytes());
+
+ Secret imagePullSecret =
+ new SecretBuilder()
+ .withType("kubernetes.io/dockerconfigjson")
+ .withNewMetadata()
+ .withName(IMAGE_PULL_SECRET_NAME)
+ .withNamespace(namespace)
+ .endMetadata()
+ .withData(Map.of(".dockerconfigjson", data))
+ .build();
+ client.resource(imagePullSecret).createOrReplace();
+ }
+
+ public Reconciler<GerritNetwork> getGerritNetworkReconciler() {
+ return new GerritNetworkReconcilerProvider(getIngressType()).get();
+ }
+
+ protected abstract IngressType getIngressType();
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/ReceiverUtil.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/ReceiverUtil.java
new file mode 100644
index 0000000..b6cdc5a
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/ReceiverUtil.java
@@ -0,0 +1,75 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.test;
+
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import io.fabric8.kubernetes.api.model.Secret;
+import io.fabric8.kubernetes.api.model.SecretBuilder;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Map;
+import org.apache.commons.codec.digest.Md5Crypt;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.apache.http.client.utils.URIBuilder;
+
+public class ReceiverUtil {
+ public static final String RECEIVER_TEST_USER = "git";
+ public static final String RECEIVER_TEST_PASSWORD = RandomStringUtils.randomAlphanumeric(32);
+ public static final String CREDENTIALS_SECRET_NAME = "receiver-secret";
+ public static final TestProperties testProps = new TestProperties();
+
+ public static int sendReceiverApiRequest(GerritCluster gerritCluster, String method, String path)
+ throws Exception {
+ URL url = getReceiverUrl(gerritCluster, path);
+
+ HttpURLConnection con = (HttpURLConnection) url.openConnection();
+ try {
+ con.setRequestMethod(method);
+ String encodedAuth =
+ Base64.getEncoder()
+ .encodeToString(
+ String.format("%s:%s", RECEIVER_TEST_USER, RECEIVER_TEST_PASSWORD)
+ .getBytes(StandardCharsets.UTF_8));
+ con.setRequestProperty("Authorization", "Basic " + encodedAuth);
+ return con.getResponseCode();
+ } finally {
+ con.disconnect();
+ }
+ }
+
+ public static URL getReceiverUrl(GerritCluster gerritCluster, String path) throws Exception {
+ return new URIBuilder()
+ .setScheme("https")
+ .setHost(gerritCluster.getSpec().getIngress().getHost())
+ .setPath(path)
+ .build()
+ .toURL();
+ }
+
+ public static Secret createCredentialsSecret(String namespace) {
+ String enPasswd = Md5Crypt.md5Crypt(RECEIVER_TEST_PASSWORD.getBytes());
+ String htpasswdContent = RECEIVER_TEST_USER + ":" + enPasswd;
+ return new SecretBuilder()
+ .withNewMetadata()
+ .withNamespace(namespace)
+ .withName(CREDENTIALS_SECRET_NAME)
+ .endMetadata()
+ .withData(
+ Map.of(".htpasswd", Base64.getEncoder().encodeToString(htpasswdContent.getBytes())))
+ .build();
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestAdmissionWebhookServer.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestAdmissionWebhookServer.java
new file mode 100644
index 0000000..90c55be
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestAdmissionWebhookServer.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.test;
+
+import com.google.gerrit.k8s.operator.server.AdmissionWebhookServlet;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.servlet.ServletHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+
+public class TestAdmissionWebhookServer {
+ public static final String KEYSTORE_PATH = "/operator/keystore.jks";
+ public static final String KEYSTORE_PWD_FILE = "/operator/keystore.password";
+ public static final int PORT = 8080;
+
+ private final Server server = new Server();
+ private final ServletHandler servletHandler = new ServletHandler();
+
+ public void start() throws Exception {
+ HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory();
+
+ ServerConnector connector = new ServerConnector(server, httpConnectionFactory);
+ connector.setPort(PORT);
+ server.setConnectors(new Connector[] {connector});
+ server.setHandler(servletHandler);
+
+ server.start();
+ }
+
+ public void registerWebhook(AdmissionWebhookServlet webhook) {
+ servletHandler.addServletWithMapping(
+ new ServletHolder(webhook), "/admission/" + webhook.getVersion() + "/" + webhook.getName());
+ }
+
+ public void stop() throws Exception {
+ server.stop();
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestGerrit.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestGerrit.java
new file mode 100644
index 0000000..5762ea5
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestGerrit.java
@@ -0,0 +1,284 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.test;
+
+import static com.google.gerrit.k8s.operator.test.TestSecureConfig.SECURE_CONFIG_SECRET_NAME;
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.k8s.operator.gerrit.dependent.GerritConfigMap;
+import com.google.gerrit.k8s.operator.gerrit.dependent.GerritInitConfigMap;
+import com.google.gerrit.k8s.operator.gerrit.dependent.GerritService;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.Gerrit;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritSite;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritSpec;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplate;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplateSpec;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplateSpec.GerritMode;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.ContainerImageConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GerritRepositoryConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GerritStorageConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.IngressConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.SharedStorage;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.StorageClassConfig;
+import io.fabric8.kubernetes.api.model.LocalObjectReference;
+import io.fabric8.kubernetes.api.model.ObjectMeta;
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+import io.fabric8.kubernetes.api.model.Quantity;
+import io.fabric8.kubernetes.api.model.ResourceRequirementsBuilder;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+
+public class TestGerrit {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ public static final TestProperties testProps = new TestProperties();
+ public static final String DEFAULT_GERRIT_CONFIG =
+ "[index]\n"
+ + " type = LUCENE\n"
+ + "[auth]\n"
+ + " type = LDAP\n"
+ + "[ldap]\n"
+ + " server = ldap://openldap.openldap.svc.cluster.local:1389\n"
+ + " accountBase = dc=example,dc=org\n"
+ + " username = cn=admin,dc=example,dc=org\n"
+ + "[httpd]\n"
+ + " requestLog = true\n"
+ + " gracefulStopTimeout = 1m\n"
+ + "[transfer]\n"
+ + " timeout = 120 s\n"
+ + "[user]\n"
+ + " name = Gerrit Code Review\n"
+ + " email = gerrit@example.com\n"
+ + " anonymousCoward = Unnamed User\n"
+ + "[container]\n"
+ + " javaOptions = -Xmx4g";
+
+ private final KubernetesClient client;
+ private final String name;
+ private final String namespace;
+ private final GerritMode mode;
+
+ private Gerrit gerrit = new Gerrit();
+ private Config config = defaultConfig();
+
+ public TestGerrit(
+ KubernetesClient client,
+ TestProperties testProps,
+ GerritMode mode,
+ String name,
+ String namespace) {
+ this.client = client;
+ this.mode = mode;
+ this.name = name;
+ this.namespace = namespace;
+ }
+
+ public TestGerrit(
+ KubernetesClient client, TestProperties testProps, String name, String namespace) {
+ this(client, testProps, GerritMode.PRIMARY, name, namespace);
+ }
+
+ public void build() {
+ createGerritCR();
+ }
+
+ public void deploy() {
+ build();
+ client.resource(gerrit).inNamespace(namespace).createOrReplace();
+ waitForGerritReadiness();
+ }
+
+ public void modifyGerritConfig(String section, String key, String value) {
+ config.setString(section, null, key, value);
+ }
+
+ public GerritSpec getSpec() {
+ return gerrit.getSpec();
+ }
+
+ public void setSpec(GerritSpec spec) {
+ gerrit.setSpec(spec);
+ deploy();
+ }
+
+ private static Config defaultConfig() {
+ Config cfg = new Config();
+ try {
+ cfg.fromText(DEFAULT_GERRIT_CONFIG);
+ } catch (ConfigInvalidException e) {
+ throw new IllegalStateException("Illegal default test configuration.");
+ }
+ return cfg;
+ }
+
+ public GerritTemplate createGerritTemplate() throws ConfigInvalidException {
+ return createGerritTemplate(name, mode, config);
+ }
+
+ public static GerritTemplate createGerritTemplate(String name, GerritMode mode)
+ throws ConfigInvalidException {
+ Config cfg = new Config();
+ cfg.fromText(DEFAULT_GERRIT_CONFIG);
+ return createGerritTemplate(name, mode, cfg);
+ }
+
+ public static GerritTemplate createGerritTemplate(String name, GerritMode mode, Config config) {
+ GerritTemplate template = new GerritTemplate();
+ ObjectMeta gerritMeta = new ObjectMetaBuilder().withName(name).build();
+ template.setMetadata(gerritMeta);
+ GerritTemplateSpec gerritSpec = template.getSpec();
+ if (gerritSpec == null) {
+ gerritSpec = new GerritTemplateSpec();
+ GerritSite site = new GerritSite();
+ site.setSize(new Quantity("1Gi"));
+ gerritSpec.setSite(site);
+ gerritSpec.setResources(
+ new ResourceRequirementsBuilder()
+ .withRequests(Map.of("cpu", new Quantity("1"), "memory", new Quantity("5Gi")))
+ .build());
+ }
+ gerritSpec.setMode(mode);
+ gerritSpec.setConfigFiles(Map.of("gerrit.config", config.toText()));
+ gerritSpec.setSecretRef(SECURE_CONFIG_SECRET_NAME);
+ template.setSpec(gerritSpec);
+ return template;
+ }
+
+ private void createGerritCR() {
+ ObjectMeta gerritMeta = new ObjectMetaBuilder().withName(name).withNamespace(namespace).build();
+ gerrit.setMetadata(gerritMeta);
+ GerritSpec gerritSpec = gerrit.getSpec();
+ if (gerritSpec == null) {
+ gerritSpec = new GerritSpec();
+ GerritSite site = new GerritSite();
+ site.setSize(new Quantity("1Gi"));
+ gerritSpec.setSite(site);
+ gerritSpec.setServerId("gerrit-1234");
+ gerritSpec.setResources(
+ new ResourceRequirementsBuilder()
+ .withRequests(Map.of("cpu", new Quantity("1"), "memory", new Quantity("5Gi")))
+ .build());
+ }
+ gerritSpec.setMode(mode);
+ gerritSpec.setConfigFiles(Map.of("gerrit.config", config.toText()));
+ gerritSpec.setSecretRef(SECURE_CONFIG_SECRET_NAME);
+
+ SharedStorage sharedStorage = new SharedStorage();
+ sharedStorage.setSize(Quantity.parse("1Gi"));
+
+ StorageClassConfig storageClassConfig = new StorageClassConfig();
+ storageClassConfig.setReadWriteMany(testProps.getRWMStorageClass());
+
+ GerritStorageConfig gerritStorageConfig = new GerritStorageConfig();
+ gerritStorageConfig.setSharedStorage(sharedStorage);
+ gerritStorageConfig.setStorageClasses(storageClassConfig);
+ gerritSpec.setStorage(gerritStorageConfig);
+
+ GerritRepositoryConfig repoConfig = new GerritRepositoryConfig();
+ repoConfig.setOrg(testProps.getRegistryOrg());
+ repoConfig.setRegistry(testProps.getRegistry());
+ repoConfig.setTag(testProps.getTag());
+
+ ContainerImageConfig containerImageConfig = new ContainerImageConfig();
+ containerImageConfig.setGerritImages(repoConfig);
+ Set<LocalObjectReference> imagePullSecrets = new HashSet<>();
+ imagePullSecrets.add(
+ new LocalObjectReference(AbstractGerritOperatorE2ETest.IMAGE_PULL_SECRET_NAME));
+ containerImageConfig.setImagePullSecrets(imagePullSecrets);
+ gerritSpec.setContainerImages(containerImageConfig);
+
+ IngressConfig ingressConfig = new IngressConfig();
+ ingressConfig.setHost(testProps.getIngressDomain());
+ ingressConfig.setTlsEnabled(false);
+ gerritSpec.setIngress(ingressConfig);
+
+ gerrit.setSpec(gerritSpec);
+ }
+
+ private void waitForGerritReadiness() {
+ logger.atInfo().log("Waiting max 1 minutes for the configmaps to be created.");
+ await()
+ .atMost(1, MINUTES)
+ .untilAsserted(
+ () -> {
+ assertThat(
+ client
+ .configMaps()
+ .inNamespace(namespace)
+ .withName(GerritConfigMap.getName(gerrit))
+ .get(),
+ is(notNullValue()));
+ assertThat(
+ client
+ .configMaps()
+ .inNamespace(namespace)
+ .withName(GerritInitConfigMap.getName(gerrit))
+ .get(),
+ is(notNullValue()));
+ });
+
+ logger.atInfo().log("Waiting max 1 minutes for the Gerrit StatefulSet to be created.");
+ await()
+ .atMost(1, MINUTES)
+ .untilAsserted(
+ () -> {
+ assertThat(
+ client
+ .apps()
+ .statefulSets()
+ .inNamespace(namespace)
+ .withName(gerrit.getMetadata().getName())
+ .get(),
+ is(notNullValue()));
+ });
+
+ logger.atInfo().log("Waiting max 1 minutes for the Gerrit Service to be created.");
+ await()
+ .atMost(1, MINUTES)
+ .untilAsserted(
+ () -> {
+ assertThat(
+ client
+ .services()
+ .inNamespace(namespace)
+ .withName(GerritService.getName(gerrit))
+ .get(),
+ is(notNullValue()));
+ });
+
+ logger.atInfo().log("Waiting max 2 minutes for the Gerrit StatefulSet to be ready.");
+ await()
+ .atMost(2, MINUTES)
+ .untilAsserted(
+ () -> {
+ assertTrue(
+ client
+ .apps()
+ .statefulSets()
+ .inNamespace(namespace)
+ .withName(gerrit.getMetadata().getName())
+ .isReady());
+ });
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestGerritCluster.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestGerritCluster.java
new file mode 100644
index 0000000..ce66ec2
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestGerritCluster.java
@@ -0,0 +1,256 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.test;
+
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.k8s.operator.network.IngressType;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.cluster.GerritClusterSpec;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.gerrit.GerritTemplate;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.receiver.ReceiverTemplate;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.ContainerImageConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GerritClusterIngressConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GerritIngressTlsConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GerritRepositoryConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.GerritStorageConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.NfsWorkaroundConfig;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.SharedStorage;
+import com.google.gerrit.k8s.operator.v1alpha.api.model.shared.StorageClassConfig;
+import com.urswolfer.gerrit.client.rest.GerritAuthData;
+import com.urswolfer.gerrit.client.rest.GerritRestApiFactory;
+import io.fabric8.kubernetes.api.model.LocalObjectReference;
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+import io.fabric8.kubernetes.api.model.Quantity;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+public class TestGerritCluster {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ public static final String CLUSTER_NAME = "test-cluster";
+ public static final TestProperties testProps = new TestProperties();
+
+ private final KubernetesClient client;
+ private final String namespace;
+
+ private GerritClusterIngressConfig ingressConfig;
+ private boolean isNfsEnabled = false;
+ private GerritCluster cluster = new GerritCluster();
+ private String hostname;
+ private List<GerritTemplate> gerrits = new ArrayList<>();
+ private Optional<ReceiverTemplate> receiver = Optional.empty();
+
+ public TestGerritCluster(KubernetesClient client, String namespace) {
+ this.client = client;
+ this.namespace = namespace;
+
+ defaultIngressConfig();
+ }
+
+ public GerritCluster getGerritCluster() {
+ return cluster;
+ }
+
+ public String getHostname() {
+ return hostname;
+ }
+
+ public String getNamespace() {
+ return cluster.getMetadata().getNamespace();
+ }
+
+ public void setIngressType(IngressType type) {
+ switch (type) {
+ case INGRESS:
+ hostname = testProps.getIngressDomain();
+ enableIngress();
+ break;
+ case ISTIO:
+ hostname = testProps.getIstioDomain();
+ enableIngress();
+ break;
+ default:
+ hostname = null;
+ defaultIngressConfig();
+ }
+ deploy();
+ }
+
+ private void defaultIngressConfig() {
+ ingressConfig = new GerritClusterIngressConfig();
+ ingressConfig.setEnabled(false);
+ }
+
+ private void enableIngress() {
+ ingressConfig = new GerritClusterIngressConfig();
+ ingressConfig.setEnabled(true);
+ ingressConfig.setHost(hostname);
+ GerritIngressTlsConfig ingressTlsConfig = new GerritIngressTlsConfig();
+ ingressTlsConfig.setEnabled(true);
+ ingressTlsConfig.setSecret("tls-secret");
+ ingressConfig.setTls(ingressTlsConfig);
+ }
+
+ public GerritApi getGerritApiClient(GerritTemplate gerrit, IngressType ingressType) {
+ return new GerritRestApiFactory()
+ .create(new GerritAuthData.Basic(String.format("https://%s", hostname)));
+ }
+
+ public void setNfsEnabled(boolean isNfsEnabled) {
+ this.isNfsEnabled = isNfsEnabled;
+ deploy();
+ }
+
+ public void addGerrit(GerritTemplate gerrit) {
+ gerrits.add(gerrit);
+ }
+
+ public void removeGerrit(GerritTemplate gerrit) {
+ gerrits.remove(gerrit);
+ }
+
+ public void setReceiver(ReceiverTemplate receiver) {
+ this.receiver = Optional.ofNullable(receiver);
+ }
+
+ public GerritCluster build() {
+ cluster.setMetadata(
+ new ObjectMetaBuilder().withName(CLUSTER_NAME).withNamespace(namespace).build());
+
+ SharedStorage sharedStorage = new SharedStorage();
+ sharedStorage.setSize(Quantity.parse("1Gi"));
+
+ StorageClassConfig storageClassConfig = new StorageClassConfig();
+ storageClassConfig.setReadWriteMany(testProps.getRWMStorageClass());
+
+ NfsWorkaroundConfig nfsWorkaround = new NfsWorkaroundConfig();
+ nfsWorkaround.setEnabled(isNfsEnabled);
+ nfsWorkaround.setIdmapdConfig("[General]\nDomain = localdomain.com");
+ storageClassConfig.setNfsWorkaround(nfsWorkaround);
+
+ GerritClusterSpec clusterSpec = new GerritClusterSpec();
+ GerritStorageConfig gerritStorageConfig = new GerritStorageConfig();
+ gerritStorageConfig.setSharedStorage(sharedStorage);
+ gerritStorageConfig.setStorageClasses(storageClassConfig);
+ clusterSpec.setStorage(gerritStorageConfig);
+
+ GerritRepositoryConfig repoConfig = new GerritRepositoryConfig();
+ repoConfig.setOrg(testProps.getRegistryOrg());
+ repoConfig.setRegistry(testProps.getRegistry());
+ repoConfig.setTag(testProps.getTag());
+
+ ContainerImageConfig containerImageConfig = new ContainerImageConfig();
+ containerImageConfig.setGerritImages(repoConfig);
+ Set<LocalObjectReference> imagePullSecrets = new HashSet<>();
+ imagePullSecrets.add(
+ new LocalObjectReference(AbstractGerritOperatorE2ETest.IMAGE_PULL_SECRET_NAME));
+ containerImageConfig.setImagePullSecrets(imagePullSecrets);
+ clusterSpec.setContainerImages(containerImageConfig);
+
+ clusterSpec.setIngress(ingressConfig);
+
+ clusterSpec.setGerrits(gerrits);
+ if (receiver.isPresent()) {
+ clusterSpec.setReceiver(receiver.get());
+ }
+
+ cluster.setSpec(clusterSpec);
+ return cluster;
+ }
+
+ public void deploy() {
+ build();
+ client.resource(cluster).inNamespace(namespace).createOrReplace();
+ await()
+ .atMost(2, MINUTES)
+ .untilAsserted(
+ () -> {
+ assertThat(
+ client
+ .resources(GerritCluster.class)
+ .inNamespace(namespace)
+ .withName(CLUSTER_NAME)
+ .get(),
+ is(notNullValue()));
+ });
+
+ GerritCluster updatedCluster =
+ client.resources(GerritCluster.class).inNamespace(namespace).withName(CLUSTER_NAME).get();
+ for (GerritTemplate gerrit : updatedCluster.getSpec().getGerrits()) {
+ waitForGerritReadiness(gerrit);
+ }
+ if (receiver.isPresent()) {
+ waitForReceiverReadiness();
+ }
+ }
+
+ private void waitForGerritReadiness(GerritTemplate gerrit) {
+ logger.atInfo().log("Waiting max 2 minutes for the Gerrit StatefulSet to be ready.");
+ await()
+ .pollDelay(15, SECONDS)
+ .atMost(2, MINUTES)
+ .untilAsserted(
+ () -> {
+ assertThat(
+ client
+ .apps()
+ .statefulSets()
+ .inNamespace(namespace)
+ .withName(gerrit.getMetadata().getName())
+ .get(),
+ is(notNullValue()));
+ assertTrue(
+ client
+ .apps()
+ .statefulSets()
+ .inNamespace(namespace)
+ .withName(gerrit.getMetadata().getName())
+ .isReady());
+ assertTrue(
+ client
+ .pods()
+ .inNamespace(namespace)
+ .withName(gerrit.getMetadata().getName() + "-0")
+ .isReady());
+ });
+ }
+
+ private void waitForReceiverReadiness() {
+ await()
+ .atMost(2, MINUTES)
+ .untilAsserted(
+ () -> {
+ assertTrue(
+ client
+ .apps()
+ .deployments()
+ .inNamespace(namespace)
+ .withName(receiver.get().getMetadata().getName())
+ .isReady());
+ });
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestProperties.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestProperties.java
new file mode 100644
index 0000000..d7d23bf
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestProperties.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.test;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.Properties;
+
+public class TestProperties {
+ private final Properties props = getProperties();
+
+ private static Properties getProperties() {
+ String propertiesPath = System.getProperty("properties", "test.properties");
+ Properties props = new Properties();
+ try {
+ props.load(new FileInputStream(propertiesPath));
+ } catch (IOException e) {
+ throw new IllegalStateException("Could not load properties file.");
+ }
+ return props;
+ }
+
+ public String getRWMStorageClass() {
+ return props.getProperty("rwmStorageClass", "nfs-client");
+ }
+
+ public String getRegistry() {
+ return props.getProperty("registry", "");
+ }
+
+ public String getRegistryOrg() {
+ return props.getProperty("registryOrg", "k8sgerrit");
+ }
+
+ public String getRegistryUser() {
+ return props.getProperty("registryUser", "");
+ }
+
+ public String getRegistryPwd() {
+ return props.getProperty("registryPwd", "");
+ }
+
+ public String getTag() {
+ return props.getProperty("tag", "");
+ }
+
+ public String getIngressDomain() {
+ return props.getProperty("ingressDomain", "");
+ }
+
+ public String getIstioDomain() {
+ return props.getProperty("istioDomain", "");
+ }
+
+ public String getLdapAdminPwd() {
+ return props.getProperty("ldapAdminPwd", "");
+ }
+
+ public String getGerritUser() {
+ return props.getProperty("gerritUser", "");
+ }
+
+ public String getGerritPwd() {
+ return props.getProperty("gerritPwd", "");
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestSecureConfig.java b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestSecureConfig.java
new file mode 100644
index 0000000..04688d7
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestSecureConfig.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.k8s.operator.test;
+
+import io.fabric8.kubernetes.api.model.Secret;
+import io.fabric8.kubernetes.api.model.SecretBuilder;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import java.util.Base64;
+import java.util.Map;
+import org.eclipse.jgit.lib.Config;
+
+public class TestSecureConfig {
+ public static final String SECURE_CONFIG_SECRET_NAME = "gerrit-secret";
+
+ private final KubernetesClient client;
+ private final String namespace;
+
+ private Config secureConfig = new Config();
+ private Secret secureConfigSecret;
+
+ public TestSecureConfig(KubernetesClient client, TestProperties testProps, String namespace) {
+ this.client = client;
+ this.namespace = namespace;
+ this.secureConfig.setString("ldap", null, "password", testProps.getLdapAdminPwd());
+ }
+
+ public void createOrReplace() {
+ secureConfigSecret =
+ new SecretBuilder()
+ .withNewMetadata()
+ .withNamespace(namespace)
+ .withName(SECURE_CONFIG_SECRET_NAME)
+ .endMetadata()
+ .withData(
+ Map.of(
+ "secure.config",
+ Base64.getEncoder().encodeToString(secureConfig.toText().getBytes())))
+ .build();
+ client.resource(secureConfigSecret).inNamespace(namespace).createOrReplace();
+ }
+
+ public void modify(String section, String key, String value) {
+ secureConfig.setString(section, null, key, value);
+ createOrReplace();
+ }
+}
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/host.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/host.yaml
new file mode 100644
index 0000000..59ff0a4
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/host.yaml
@@ -0,0 +1,9 @@
+apiVersion: getambassador.io/v2
+kind: Host
+metadata:
+ name: gerrit-ambassador-host
+spec:
+ ambassador_id:
+ - my-id-1
+ - my-id-2
+ hostname: example.com
\ No newline at end of file
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/host_with_tls.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/host_with_tls.yaml
new file mode 100644
index 0000000..c386b3a
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/host_with_tls.yaml
@@ -0,0 +1,13 @@
+apiVersion: getambassador.io/v2
+kind: Host
+metadata:
+ name: gerrit-ambassador-host
+spec:
+ ambassador_id:
+ - my-id-1
+ - my-id-2
+ hostname: example.com
+ tlsContext:
+ name: gerrit-tls-context
+ tlsSecret:
+ name: tls-secret
\ No newline at end of file
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingGETReplica_primary_replica.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingGETReplica_primary_replica.yaml
new file mode 100644
index 0000000..841d1fb
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingGETReplica_primary_replica.yaml
@@ -0,0 +1,18 @@
+apiVersion: getambassador.io/v2
+kind: Mapping
+metadata:
+ name: gerrit-mapping-get-replica
+ namespace: gerrit
+spec:
+ ambassador_id:
+ - my-id-1
+ - my-id-2
+ bypass_auth: true
+ rewrite: ""
+ host: example.com
+ method: GET
+ prefix: /.*/info/refs
+ prefix_regex: true
+ query_parameters:
+ service: git-upload-pack
+ service: replica:48080
\ No newline at end of file
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingGET_receiver.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingGET_receiver.yaml
new file mode 100644
index 0000000..b179763
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingGET_receiver.yaml
@@ -0,0 +1,18 @@
+apiVersion: getambassador.io/v2
+kind: Mapping
+metadata:
+ name: gerrit-mapping-receiver-get
+ namespace: gerrit
+spec:
+ ambassador_id:
+ - my-id-1
+ - my-id-2
+ bypass_auth: true
+ rewrite: ""
+ host: example.com
+ method: GET
+ prefix: /.*/info/refs
+ prefix_regex: true
+ query_parameters:
+ service: git-receive-pack
+ service: receiver:48081
\ No newline at end of file
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingPOSTReplica_primary_replica.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingPOSTReplica_primary_replica.yaml
new file mode 100644
index 0000000..02f717a
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingPOSTReplica_primary_replica.yaml
@@ -0,0 +1,16 @@
+apiVersion: getambassador.io/v2
+kind: Mapping
+metadata:
+ name: gerrit-mapping-post-replica
+ namespace: gerrit
+spec:
+ ambassador_id:
+ - my-id-1
+ - my-id-2
+ bypass_auth: true
+ rewrite: ""
+ host: example.com
+ method: POST
+ prefix: /.*/git-upload-pack
+ prefix_regex: true
+ service: replica:48080
\ No newline at end of file
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingPrimary_primary_replica.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingPrimary_primary_replica.yaml
new file mode 100644
index 0000000..e6f52a5
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mappingPrimary_primary_replica.yaml
@@ -0,0 +1,14 @@
+apiVersion: getambassador.io/v2
+kind: Mapping
+metadata:
+ name: gerrit-mapping-primary
+ namespace: gerrit
+spec:
+ ambassador_id:
+ - my-id-1
+ - my-id-2
+ bypass_auth: true
+ rewrite: ""
+ host: example.com
+ prefix: /
+ service: primary:48080
\ No newline at end of file
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mapping_primary.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mapping_primary.yaml
new file mode 100644
index 0000000..4ea6c95
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mapping_primary.yaml
@@ -0,0 +1,14 @@
+apiVersion: getambassador.io/v2
+kind: Mapping
+metadata:
+ name: gerrit-mapping
+ namespace: gerrit
+spec:
+ ambassador_id:
+ - my-id-1
+ - my-id-2
+ bypass_auth: true
+ rewrite: ""
+ host: example.com
+ prefix: /
+ service: primary:48080
\ No newline at end of file
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mapping_receiver.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mapping_receiver.yaml
new file mode 100644
index 0000000..93b9eed
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mapping_receiver.yaml
@@ -0,0 +1,15 @@
+apiVersion: getambassador.io/v2
+kind: Mapping
+metadata:
+ name: gerrit-mapping-receiver
+ namespace: gerrit
+spec:
+ ambassador_id:
+ - my-id-1
+ - my-id-2
+ bypass_auth: true
+ rewrite: ""
+ host: example.com
+ prefix: /a/projects/.*|/.*/git-receive-pack
+ prefix_regex: true
+ service: receiver:48081
\ No newline at end of file
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mapping_replica.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mapping_replica.yaml
new file mode 100644
index 0000000..7fb2b7b
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/mapping_replica.yaml
@@ -0,0 +1,14 @@
+apiVersion: getambassador.io/v2
+kind: Mapping
+metadata:
+ name: gerrit-mapping
+ namespace: gerrit
+spec:
+ ambassador_id:
+ - my-id-1
+ - my-id-2
+ bypass_auth: true
+ rewrite: ""
+ host: example.com
+ prefix: /
+ service: replica:48080
\ No newline at end of file
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/tlscontext.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/tlscontext.yaml
new file mode 100644
index 0000000..aee8ddb
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ambassador/dependent/tlscontext.yaml
@@ -0,0 +1,13 @@
+apiVersion: getambassador.io/v2
+kind: TLSContext
+metadata:
+ name: gerrit-tls-context
+ namespace: gerrit
+spec:
+ ambassador_id:
+ - my-id-1
+ - my-id-2
+ secret: tls-secret
+ secret_namespacing: true
+ hosts:
+ - example.com
\ No newline at end of file
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary.yaml
new file mode 100644
index 0000000..68b81f5
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary.yaml
@@ -0,0 +1,17 @@
+apiVersion: "gerritoperator.google.com/v1alpha1"
+kind: GerritNetwork
+metadata:
+ name: gerrit
+ namespace: gerrit
+spec:
+ ingress:
+ enabled: true
+ host: example.com
+ tls:
+ enabled: false
+ ambassador:
+ id: ["my-id-1", "my-id-2"]
+ primaryGerrit:
+ name: primary
+ httpPort: 48080
+ sshPort: 49418
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica.yaml
new file mode 100644
index 0000000..443e465
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica.yaml
@@ -0,0 +1,21 @@
+apiVersion: "gerritoperator.google.com/v1alpha1"
+kind: GerritNetwork
+metadata:
+ name: gerrit
+ namespace: gerrit
+spec:
+ ingress:
+ enabled: true
+ host: example.com
+ tls:
+ enabled: false
+ ambassador:
+ id: ["my-id-1", "my-id-2"]
+ primaryGerrit:
+ name: primary
+ httpPort: 48080
+ sshPort: 49418
+ gerritReplica:
+ name: replica
+ httpPort: 48080
+ sshPort: 49418
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica_create_host.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica_create_host.yaml
new file mode 100644
index 0000000..ef4f76c
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica_create_host.yaml
@@ -0,0 +1,22 @@
+apiVersion: "gerritoperator.google.com/v1alpha1"
+kind: GerritNetwork
+metadata:
+ name: gerrit
+ namespace: gerrit
+spec:
+ ingress:
+ enabled: true
+ host: example.com
+ tls:
+ enabled: false
+ ambassador:
+ id: ["my-id-1", "my-id-2"]
+ createHost: true
+ primaryGerrit:
+ name: primary
+ httpPort: 48080
+ sshPort: 49418
+ gerritReplica:
+ name: replica
+ httpPort: 48080
+ sshPort: 49418
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica_ssh.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica_ssh.yaml
new file mode 100644
index 0000000..623890f
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica_ssh.yaml
@@ -0,0 +1,21 @@
+apiVersion: "gerritoperator.google.com/v1alpha1"
+kind: GerritNetwork
+metadata:
+ name: gerrit
+ namespace: gerrit
+spec:
+ ingress:
+ enabled: true
+ host: example.com
+ tls:
+ enabled: false
+ ssh:
+ enabled: true
+ primaryGerrit:
+ name: primary
+ httpPort: 48080
+ sshPort: 49418
+ gerritReplica:
+ name: replica
+ httpPort: 48080
+ sshPort: 49419
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica_tls.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica_tls.yaml
new file mode 100644
index 0000000..21b9e59
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica_tls.yaml
@@ -0,0 +1,22 @@
+apiVersion: "gerritoperator.google.com/v1alpha1"
+kind: GerritNetwork
+metadata:
+ name: gerrit
+ namespace: gerrit
+spec:
+ ingress:
+ enabled: true
+ host: example.com
+ tls:
+ enabled: true
+ secret: tls-secret
+ ambassador:
+ id: ["my-id-1", "my-id-2"]
+ primaryGerrit:
+ name: primary
+ httpPort: 48080
+ sshPort: 49418
+ gerritReplica:
+ name: replica
+ httpPort: 48080
+ sshPort: 49418
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica_tls_create_host.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica_tls_create_host.yaml
new file mode 100644
index 0000000..c84e3cc
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_replica_tls_create_host.yaml
@@ -0,0 +1,23 @@
+apiVersion: "gerritoperator.google.com/v1alpha1"
+kind: GerritNetwork
+metadata:
+ name: gerrit
+ namespace: gerrit
+spec:
+ ingress:
+ enabled: true
+ host: example.com
+ tls:
+ enabled: true
+ secret: tls-secret
+ ambassador:
+ id: ["my-id-1", "my-id-2"]
+ createHost: true
+ primaryGerrit:
+ name: primary
+ httpPort: 48080
+ sshPort: 49418
+ gerritReplica:
+ name: replica
+ httpPort: 48080
+ sshPort: 49418
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_ssh.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_ssh.yaml
new file mode 100644
index 0000000..a6b3271
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_primary_ssh.yaml
@@ -0,0 +1,17 @@
+apiVersion: "gerritoperator.google.com/v1alpha1"
+kind: GerritNetwork
+metadata:
+ name: gerrit
+ namespace: gerrit
+spec:
+ ingress:
+ enabled: true
+ host: example.com
+ tls:
+ enabled: false
+ ssh:
+ enabled: true
+ primaryGerrit:
+ name: primary
+ httpPort: 48080
+ sshPort: 49418
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_receiver_replica.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_receiver_replica.yaml
new file mode 100644
index 0000000..e6da865
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_receiver_replica.yaml
@@ -0,0 +1,20 @@
+apiVersion: "gerritoperator.google.com/v1alpha1"
+kind: GerritNetwork
+metadata:
+ name: gerrit
+ namespace: gerrit
+spec:
+ ingress:
+ enabled: true
+ host: example.com
+ tls:
+ enabled: false
+ ambassador:
+ id: ["my-id-1", "my-id-2"]
+ gerritReplica:
+ name: replica
+ httpPort: 48080
+ sshPort: 49418
+ receiver:
+ name: receiver
+ httpPort: 48081
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_receiver_replica_ssh.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_receiver_replica_ssh.yaml
new file mode 100644
index 0000000..7efc649
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_receiver_replica_ssh.yaml
@@ -0,0 +1,20 @@
+apiVersion: "gerritoperator.google.com/v1alpha1"
+kind: GerritNetwork
+metadata:
+ name: gerrit
+ namespace: gerrit
+spec:
+ ingress:
+ enabled: true
+ host: example.com
+ tls:
+ enabled: false
+ ssh:
+ enabled: true
+ gerritReplica:
+ name: replica
+ httpPort: 48080
+ sshPort: 49419
+ receiver:
+ name: receiver
+ httpPort: 48081
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_receiver_replica_tls.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_receiver_replica_tls.yaml
new file mode 100644
index 0000000..d5bd3c0
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_receiver_replica_tls.yaml
@@ -0,0 +1,21 @@
+apiVersion: "gerritoperator.google.com/v1alpha1"
+kind: GerritNetwork
+metadata:
+ name: gerrit
+ namespace: gerrit
+spec:
+ ingress:
+ enabled: true
+ host: example.com
+ tls:
+ enabled: true
+ secret: tls-secret
+ ambassador:
+ id: ["my-id-1", "my-id-2"]
+ gerritReplica:
+ name: replica
+ httpPort: 48080
+ sshPort: 49418
+ receiver:
+ name: receiver
+ httpPort: 48081
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_replica.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_replica.yaml
new file mode 100644
index 0000000..915da97
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_replica.yaml
@@ -0,0 +1,17 @@
+apiVersion: "gerritoperator.google.com/v1alpha1"
+kind: GerritNetwork
+metadata:
+ name: gerrit
+ namespace: gerrit
+spec:
+ ingress:
+ enabled: true
+ host: example.com
+ tls:
+ enabled: false
+ ambassador:
+ id: ["my-id-1", "my-id-2"]
+ gerritReplica:
+ name: replica
+ httpPort: 48080
+ sshPort: 49418
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_replica_ssh.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_replica_ssh.yaml
new file mode 100644
index 0000000..efa54a7
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/gerritnetwork_replica_ssh.yaml
@@ -0,0 +1,17 @@
+apiVersion: "gerritoperator.google.com/v1alpha1"
+kind: GerritNetwork
+metadata:
+ name: gerrit
+ namespace: gerrit
+spec:
+ ingress:
+ enabled: true
+ host: example.com
+ tls:
+ enabled: false
+ ssh:
+ enabled: true
+ gerritReplica:
+ name: replica
+ httpPort: 48080
+ sshPort: 49419
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_primary.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_primary.yaml
new file mode 100644
index 0000000..9b18d5c
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_primary.yaml
@@ -0,0 +1,25 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: gerrit-ingress
+ namespace: gerrit
+ annotations:
+ nginx.ingress.kubernetes.io/use-regex: true
+ kubernetes.io/ingress.class: nginx
+ nginx.ingress.kubernetes.io/affinity: cookie
+ nginx.ingress.kubernetes.io/session-cookie-name: Gerrit_Session
+ nginx.ingress.kubernetes.io/session-cookie-path: /
+ nginx.ingress.kubernetes.io/session-cookie-max-age: 60
+ nginx.ingress.kubernetes.io/session-cookie-expires: 60
+spec:
+ rules:
+ - host: example.com
+ http:
+ paths:
+ - pathType: Prefix
+ path: "/"
+ backend:
+ service:
+ name: primary
+ port:
+ name: http
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_primary_replica.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_primary_replica.yaml
new file mode 100644
index 0000000..d6aae78
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_primary_replica.yaml
@@ -0,0 +1,38 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: gerrit-ingress
+ namespace: gerrit
+ annotations:
+ nginx.ingress.kubernetes.io/use-regex: true
+ kubernetes.io/ingress.class: nginx
+ nginx.ingress.kubernetes.io/configuration-snippet: |-
+ if ($args ~ service=git-upload-pack){
+ set $proxy_upstream_name "gerrit-replica-http";
+ set $proxy_host $proxy_upstream_name;
+ set $service_name "replica";
+ }
+ nginx.ingress.kubernetes.io/affinity: cookie
+ nginx.ingress.kubernetes.io/session-cookie-name: Gerrit_Session
+ nginx.ingress.kubernetes.io/session-cookie-path: /
+ nginx.ingress.kubernetes.io/session-cookie-max-age: 60
+ nginx.ingress.kubernetes.io/session-cookie-expires: 60
+spec:
+ rules:
+ - host: example.com
+ http:
+ paths:
+ - pathType: Prefix
+ path: "/.*/git-upload-pack"
+ backend:
+ service:
+ name: replica
+ port:
+ name: http
+ - pathType: Prefix
+ path: "/"
+ backend:
+ service:
+ name: primary
+ port:
+ name: http
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_primary_replica_tls.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_primary_replica_tls.yaml
new file mode 100644
index 0000000..adbe808
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_primary_replica_tls.yaml
@@ -0,0 +1,42 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: gerrit-ingress
+ namespace: gerrit
+ annotations:
+ nginx.ingress.kubernetes.io/use-regex: true
+ kubernetes.io/ingress.class: nginx
+ nginx.ingress.kubernetes.io/configuration-snippet: |-
+ if ($args ~ service=git-upload-pack){
+ set $proxy_upstream_name "gerrit-replica-http";
+ set $proxy_host $proxy_upstream_name;
+ set $service_name "replica";
+ }
+ nginx.ingress.kubernetes.io/affinity: cookie
+ nginx.ingress.kubernetes.io/session-cookie-name: Gerrit_Session
+ nginx.ingress.kubernetes.io/session-cookie-path: /
+ nginx.ingress.kubernetes.io/session-cookie-max-age: 60
+ nginx.ingress.kubernetes.io/session-cookie-expires: 60
+spec:
+ tls:
+ - hosts:
+ - example.com
+ secretName: tls-secret
+ rules:
+ - host: example.com
+ http:
+ paths:
+ - pathType: Prefix
+ path: "/.*/git-upload-pack"
+ backend:
+ service:
+ name: replica
+ port:
+ name: http
+ - pathType: Prefix
+ path: "/"
+ backend:
+ service:
+ name: primary
+ port:
+ name: http
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_receiver_replica.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_receiver_replica.yaml
new file mode 100644
index 0000000..75a37c7
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_receiver_replica.yaml
@@ -0,0 +1,45 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: gerrit-ingress
+ namespace: gerrit
+ annotations:
+ nginx.ingress.kubernetes.io/use-regex: true
+ kubernetes.io/ingress.class: nginx
+ nginx.ingress.kubernetes.io/affinity: cookie
+ nginx.ingress.kubernetes.io/session-cookie-name: Gerrit_Session
+ nginx.ingress.kubernetes.io/session-cookie-path: /
+ nginx.ingress.kubernetes.io/session-cookie-max-age: 60
+ nginx.ingress.kubernetes.io/session-cookie-expires: 60
+ nginx.ingress.kubernetes.io/configuration-snippet: |-
+ if ($args ~ service=git-receive-pack){
+ set $proxy_upstream_name "gerrit-receiver-http";
+ set $proxy_host $proxy_upstream_name;
+ set $service_name "receiver";
+ }
+spec:
+ rules:
+ - host: example.com
+ http:
+ paths:
+ - pathType: Prefix
+ path: "/a/projects"
+ backend:
+ service:
+ name: receiver
+ port:
+ name: http
+ - pathType: Prefix
+ path: "/.*/git-receive-pack"
+ backend:
+ service:
+ name: receiver
+ port:
+ name: http
+ - pathType: Prefix
+ path: "/"
+ backend:
+ service:
+ name: replica
+ port:
+ name: http
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_receiver_replica_tls.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_receiver_replica_tls.yaml
new file mode 100644
index 0000000..ad4cfbf
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_receiver_replica_tls.yaml
@@ -0,0 +1,49 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: gerrit-ingress
+ namespace: gerrit
+ annotations:
+ nginx.ingress.kubernetes.io/use-regex: true
+ kubernetes.io/ingress.class: nginx
+ nginx.ingress.kubernetes.io/affinity: cookie
+ nginx.ingress.kubernetes.io/session-cookie-name: Gerrit_Session
+ nginx.ingress.kubernetes.io/session-cookie-path: /
+ nginx.ingress.kubernetes.io/session-cookie-max-age: 60
+ nginx.ingress.kubernetes.io/session-cookie-expires: 60
+ nginx.ingress.kubernetes.io/configuration-snippet: |-
+ if ($args ~ service=git-receive-pack){
+ set $proxy_upstream_name "gerrit-receiver-http";
+ set $proxy_host $proxy_upstream_name;
+ set $service_name "receiver";
+ }
+spec:
+ tls:
+ - hosts:
+ - example.com
+ secretName: tls-secret
+ rules:
+ - host: example.com
+ http:
+ paths:
+ - pathType: Prefix
+ path: "/a/projects"
+ backend:
+ service:
+ name: receiver
+ port:
+ name: http
+ - pathType: Prefix
+ path: "/.*/git-receive-pack"
+ backend:
+ service:
+ name: receiver
+ port:
+ name: http
+ - pathType: Prefix
+ path: "/"
+ backend:
+ service:
+ name: replica
+ port:
+ name: http
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_replica.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_replica.yaml
new file mode 100644
index 0000000..07dfe7d
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/ingress/dependent/ingress_replica.yaml
@@ -0,0 +1,25 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: gerrit-ingress
+ namespace: gerrit
+ annotations:
+ nginx.ingress.kubernetes.io/use-regex: true
+ kubernetes.io/ingress.class: nginx
+ nginx.ingress.kubernetes.io/affinity: cookie
+ nginx.ingress.kubernetes.io/session-cookie-name: Gerrit_Session
+ nginx.ingress.kubernetes.io/session-cookie-path: /
+ nginx.ingress.kubernetes.io/session-cookie-max-age: 60
+ nginx.ingress.kubernetes.io/session-cookie-expires: 60
+spec:
+ rules:
+ - host: example.com
+ http:
+ paths:
+ - pathType: Prefix
+ path: "/"
+ backend:
+ service:
+ name: replica
+ port:
+ name: http
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway.yaml
new file mode 100644
index 0000000..ccbab63
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway.yaml
@@ -0,0 +1,17 @@
+apiVersion: networking.istio.io/v1beta1
+kind: Gateway
+metadata:
+ name: gerrit-istio-gateway
+ namespace: gerrit
+spec:
+ selector:
+ istio: ingressgateway
+ servers:
+ - port:
+ number: 80
+ name: http
+ protocol: HTTP
+ hosts:
+ - example.com
+ tls:
+ httpsRedirect: false
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_primary_replica_ssh.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_primary_replica_ssh.yaml
new file mode 100644
index 0000000..70a38f2
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_primary_replica_ssh.yaml
@@ -0,0 +1,29 @@
+apiVersion: networking.istio.io/v1beta1
+kind: Gateway
+metadata:
+ name: gerrit-istio-gateway
+ namespace: gerrit
+spec:
+ selector:
+ istio: ingressgateway
+ servers:
+ - port:
+ number: 80
+ name: http
+ protocol: HTTP
+ hosts:
+ - example.com
+ tls:
+ httpsRedirect: false
+ - port:
+ number: 49418
+ name: ssh-primary
+ protocol: TCP
+ hosts:
+ - example.com
+ - port:
+ number: 49419
+ name: ssh-replica
+ protocol: TCP
+ hosts:
+ - example.com
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_primary_ssh.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_primary_ssh.yaml
new file mode 100644
index 0000000..7815aba
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_primary_ssh.yaml
@@ -0,0 +1,23 @@
+apiVersion: networking.istio.io/v1beta1
+kind: Gateway
+metadata:
+ name: gerrit-istio-gateway
+ namespace: gerrit
+spec:
+ selector:
+ istio: ingressgateway
+ servers:
+ - port:
+ number: 80
+ name: http
+ protocol: HTTP
+ hosts:
+ - example.com
+ tls:
+ httpsRedirect: false
+ - port:
+ number: 49418
+ name: ssh-primary
+ protocol: TCP
+ hosts:
+ - example.com
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_receiver_replica_ssh.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_receiver_replica_ssh.yaml
new file mode 100644
index 0000000..04212e3
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_receiver_replica_ssh.yaml
@@ -0,0 +1,23 @@
+apiVersion: networking.istio.io/v1beta1
+kind: Gateway
+metadata:
+ name: gerrit-istio-gateway
+ namespace: gerrit
+spec:
+ selector:
+ istio: ingressgateway
+ servers:
+ - port:
+ number: 80
+ name: http
+ protocol: HTTP
+ hosts:
+ - example.com
+ tls:
+ httpsRedirect: false
+ - port:
+ number: 49419
+ name: ssh-replica
+ protocol: TCP
+ hosts:
+ - example.com
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_replica_ssh.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_replica_ssh.yaml
new file mode 100644
index 0000000..04212e3
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_replica_ssh.yaml
@@ -0,0 +1,23 @@
+apiVersion: networking.istio.io/v1beta1
+kind: Gateway
+metadata:
+ name: gerrit-istio-gateway
+ namespace: gerrit
+spec:
+ selector:
+ istio: ingressgateway
+ servers:
+ - port:
+ number: 80
+ name: http
+ protocol: HTTP
+ hosts:
+ - example.com
+ tls:
+ httpsRedirect: false
+ - port:
+ number: 49419
+ name: ssh-replica
+ protocol: TCP
+ hosts:
+ - example.com
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_tls.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_tls.yaml
new file mode 100644
index 0000000..9f64ad5
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/gateway_tls.yaml
@@ -0,0 +1,26 @@
+apiVersion: networking.istio.io/v1beta1
+kind: Gateway
+metadata:
+ name: gerrit-istio-gateway
+ namespace: gerrit
+spec:
+ selector:
+ istio: ingressgateway
+ servers:
+ - port:
+ number: 80
+ name: http
+ protocol: HTTP
+ hosts:
+ - example.com
+ tls:
+ httpsRedirect: true
+ - port:
+ number: 443
+ name: https
+ protocol: HTTPS
+ hosts:
+ - example.com
+ tls:
+ mode: SIMPLE
+ credentialName: tls-secret
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_primary.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_primary.yaml
new file mode 100644
index 0000000..ab1bf7a
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_primary.yaml
@@ -0,0 +1,17 @@
+apiVersion: networking.istio.io/v1beta1
+kind: VirtualService
+metadata:
+ name: gerrit-gerrit-http-virtual-service
+ namespace: gerrit
+spec:
+ hosts:
+ - example.com
+ gateways:
+ - gerrit/gerrit-istio-gateway
+ http:
+ - name: gerrit-primary-primary
+ route:
+ - destination:
+ port:
+ number: 48080
+ host: primary.gerrit.svc.cluster.local
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_primary_replica.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_primary_replica.yaml
new file mode 100644
index 0000000..f97524d
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_primary_replica.yaml
@@ -0,0 +1,37 @@
+apiVersion: networking.istio.io/v1beta1
+kind: VirtualService
+metadata:
+ name: gerrit-gerrit-http-virtual-service
+ namespace: gerrit
+spec:
+ hosts:
+ - example.com
+ gateways:
+ - gerrit/gerrit-istio-gateway
+ http:
+ - name: gerrit-replica-replica
+ match:
+ - uri:
+ regex: "^/(.*)/info/refs$"
+ queryParams:
+ service:
+ exact: git-upload-pack
+ ignoreUriCase: true
+ method:
+ exact: GET
+ - uri:
+ regex: "^/(.*)/git-upload-pack$"
+ ignoreUriCase: true
+ method:
+ exact: POST
+ route:
+ - destination:
+ port:
+ number: 48080
+ host: replica.gerrit.svc.cluster.local
+ - name: gerrit-primary-primary
+ route:
+ - destination:
+ port:
+ number: 48080
+ host: primary.gerrit.svc.cluster.local
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_primary_replica_ssh.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_primary_replica_ssh.yaml
new file mode 100644
index 0000000..30ffb44
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_primary_replica_ssh.yaml
@@ -0,0 +1,52 @@
+apiVersion: networking.istio.io/v1beta1
+kind: VirtualService
+metadata:
+ name: gerrit-gerrit-http-virtual-service
+ namespace: gerrit
+spec:
+ hosts:
+ - example.com
+ gateways:
+ - gerrit/gerrit-istio-gateway
+ http:
+ - name: gerrit-replica-replica
+ match:
+ - uri:
+ regex: "^/(.*)/info/refs$"
+ queryParams:
+ service:
+ exact: git-upload-pack
+ ignoreUriCase: true
+ method:
+ exact: GET
+ - uri:
+ regex: "^/(.*)/git-upload-pack$"
+ ignoreUriCase: true
+ method:
+ exact: POST
+ route:
+ - destination:
+ port:
+ number: 48080
+ host: replica.gerrit.svc.cluster.local
+ - name: gerrit-primary-primary
+ route:
+ - destination:
+ port:
+ number: 48080
+ host: primary.gerrit.svc.cluster.local
+ tcp:
+ - match:
+ - port: 49418
+ route:
+ - destination:
+ port:
+ number: 49418
+ host: primary.gerrit.svc.cluster.local
+ - match:
+ - port: 49419
+ route:
+ - destination:
+ port:
+ number: 49419
+ host: replica.gerrit.svc.cluster.local
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_primary_ssh.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_primary_ssh.yaml
new file mode 100644
index 0000000..73f5bd7
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_primary_ssh.yaml
@@ -0,0 +1,25 @@
+apiVersion: networking.istio.io/v1beta1
+kind: VirtualService
+metadata:
+ name: gerrit-gerrit-http-virtual-service
+ namespace: gerrit
+spec:
+ hosts:
+ - example.com
+ gateways:
+ - gerrit/gerrit-istio-gateway
+ http:
+ - name: gerrit-primary-primary
+ route:
+ - destination:
+ port:
+ number: 48080
+ host: primary.gerrit.svc.cluster.local
+ tcp:
+ - match:
+ - port: 49418
+ route:
+ - destination:
+ port:
+ number: 49418
+ host: primary.gerrit.svc.cluster.local
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_receiver_replica.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_receiver_replica.yaml
new file mode 100644
index 0000000..3a224d6
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_receiver_replica.yaml
@@ -0,0 +1,33 @@
+apiVersion: networking.istio.io/v1beta1
+kind: VirtualService
+metadata:
+ name: gerrit-gerrit-http-virtual-service
+ namespace: gerrit
+spec:
+ hosts:
+ - example.com
+ gateways:
+ - gerrit/gerrit-istio-gateway
+ http:
+ - name: receiver-receiver
+ match:
+ - uri:
+ prefix: "/a/projects/"
+ - uri:
+ regex: "^/(.*)/git-receive-pack$"
+ - uri:
+ regex: "^/(.*)/info/refs$"
+ queryParams:
+ service:
+ exact: git-receive-pack
+ route:
+ - destination:
+ port:
+ number: 48081
+ host: receiver.gerrit.svc.cluster.local
+ - name: gerrit-replica-replica
+ route:
+ - destination:
+ port:
+ number: 48080
+ host: replica.gerrit.svc.cluster.local
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_receiver_replica_ssh.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_receiver_replica_ssh.yaml
new file mode 100644
index 0000000..422e2e9
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_receiver_replica_ssh.yaml
@@ -0,0 +1,41 @@
+apiVersion: networking.istio.io/v1beta1
+kind: VirtualService
+metadata:
+ name: gerrit-gerrit-http-virtual-service
+ namespace: gerrit
+spec:
+ hosts:
+ - example.com
+ gateways:
+ - gerrit/gerrit-istio-gateway
+ http:
+ - name: receiver-receiver
+ match:
+ - uri:
+ prefix: "/a/projects/"
+ - uri:
+ regex: "^/(.*)/git-receive-pack$"
+ - uri:
+ regex: "^/(.*)/info/refs$"
+ queryParams:
+ service:
+ exact: git-receive-pack
+ route:
+ - destination:
+ port:
+ number: 48081
+ host: receiver.gerrit.svc.cluster.local
+ - name: gerrit-replica-replica
+ route:
+ - destination:
+ port:
+ number: 48080
+ host: replica.gerrit.svc.cluster.local
+ tcp:
+ - match:
+ - port: 49419
+ route:
+ - destination:
+ port:
+ number: 49419
+ host: replica.gerrit.svc.cluster.local
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_replica.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_replica.yaml
new file mode 100644
index 0000000..d077def
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_replica.yaml
@@ -0,0 +1,17 @@
+apiVersion: networking.istio.io/v1beta1
+kind: VirtualService
+metadata:
+ name: gerrit-gerrit-http-virtual-service
+ namespace: gerrit
+spec:
+ hosts:
+ - example.com
+ gateways:
+ - gerrit/gerrit-istio-gateway
+ http:
+ - name: gerrit-replica-replica
+ route:
+ - destination:
+ port:
+ number: 48080
+ host: replica.gerrit.svc.cluster.local
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_replica_ssh.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_replica_ssh.yaml
new file mode 100644
index 0000000..d9e7fd3
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/network/istio/dependent/virtualservice_replica_ssh.yaml
@@ -0,0 +1,25 @@
+apiVersion: networking.istio.io/v1beta1
+kind: VirtualService
+metadata:
+ name: gerrit-gerrit-http-virtual-service
+ namespace: gerrit
+spec:
+ hosts:
+ - example.com
+ gateways:
+ - gerrit/gerrit-istio-gateway
+ http:
+ - name: gerrit-replica-replica
+ route:
+ - destination:
+ port:
+ number: 48080
+ host: replica.gerrit.svc.cluster.local
+ tcp:
+ - match:
+ - port: 49419
+ route:
+ - destination:
+ port:
+ number: 49419
+ host: replica.gerrit.svc.cluster.local
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/dependent/deployment.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/dependent/deployment.yaml
new file mode 100644
index 0000000..4857152
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/dependent/deployment.yaml
@@ -0,0 +1,118 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: receiver
+ namespace: gerrit
+ labels:
+ app.kubernetes.io/managed-by: gerrit-operator
+ app.kubernetes.io/name: gerrit
+ app.kubernetes.io/part-of: receiver
+ app.kubernetes.io/created-by: ReceiverReconciler
+ app.kubernetes.io/instance: receiver
+ app.kubernetes.io/version: unknown
+ app.kubernetes.io/component: receiver-deployment-receiver
+spec:
+ replicas: 1
+ strategy:
+ rollingUpdate:
+ maxSurge: 1
+ maxUnavailable: 1
+ selector:
+ matchLabels:
+ app.kubernetes.io/managed-by: gerrit-operator
+ app.kubernetes.io/name: gerrit
+ app.kubernetes.io/part-of: receiver
+ app.kubernetes.io/instance: receiver
+ app.kubernetes.io/component: receiver-deployment-receiver
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/managed-by: gerrit-operator
+ app.kubernetes.io/name: gerrit
+ app.kubernetes.io/part-of: receiver
+ app.kubernetes.io/created-by: ReceiverReconciler
+ app.kubernetes.io/instance: receiver
+ app.kubernetes.io/version: unknown
+ app.kubernetes.io/component: receiver-deployment-receiver
+ spec:
+ tolerations:
+ - key: key1
+ operator: Equal
+ value: value1
+ effect: NoSchedule
+ topologySpreadConstraints:
+ - maxSkew: 1
+ topologyKey: zone
+ whenUnsatisfiable: DoNotSchedule
+ labelSelector:
+ matchLabels:
+ foo: bar
+ affinity:
+ nodeAffinity:
+ requiredDuringSchedulingIgnoredDuringExecution:
+ nodeSelectorTerms:
+ - matchExpressions:
+ - key: disktype
+ operator: In
+ values:
+ - ssd
+ priorityClassName: prio
+ securityContext:
+ fsGroup: 100
+ imagePullSecrets: []
+ initContainers: []
+ containers:
+ - name: apache-git-http-backend
+ imagePullPolicy: Always
+ image: docker.io/k8sgerrit/apache-git-http-backend:latest
+ env:
+ - name: POD_NAME
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.name
+ ports:
+ - name: http
+ containerPort: 80
+ resources:
+ requests:
+ cpu: 1
+ memory: 5Gi
+ limits:
+ cpu: 1
+ memory: 6Gi
+
+ readinessProbe:
+ tcpSocket:
+ port: 80
+ initialDelaySeconds: 0
+ periodSeconds: 10
+ timeoutSeconds: 1
+ successThreshold: 1
+ failureThreshold: 3
+
+ livenessProbe:
+ tcpSocket:
+ port: 80
+ initialDelaySeconds: 0
+ periodSeconds: 10
+ timeoutSeconds: 1
+ successThreshold: 1
+ failureThreshold: 3
+
+ volumeMounts:
+ - name: shared
+ subPathExpr: "logs/$(POD_NAME)"
+ mountPath: /var/log/apache2
+ - name: apache-credentials
+ mountPath: /var/apache/credentials/.htpasswd
+ subPath: .htpasswd
+ - name: shared
+ subPath: git
+ mountPath: /var/gerrit/git
+ volumes:
+ - name: shared
+ persistentVolumeClaim:
+ claimName: shared-pvc
+ - name: apache-credentials
+ secret:
+ secretName: apache-credentials
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/dependent/deployment_minimal.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/dependent/deployment_minimal.yaml
new file mode 100644
index 0000000..92d0752
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/dependent/deployment_minimal.yaml
@@ -0,0 +1,79 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: receiver
+ namespace: gerrit
+ labels:
+ app.kubernetes.io/managed-by: gerrit-operator
+ app.kubernetes.io/name: gerrit
+ app.kubernetes.io/part-of: receiver
+ app.kubernetes.io/created-by: ReceiverReconciler
+ app.kubernetes.io/instance: receiver
+ app.kubernetes.io/version: unknown
+ app.kubernetes.io/component: receiver-deployment-receiver
+spec:
+ replicas: 1
+ strategy:
+ rollingUpdate:
+ maxSurge: 1
+ maxUnavailable: 1
+ selector:
+ matchLabels:
+ app.kubernetes.io/managed-by: gerrit-operator
+ app.kubernetes.io/name: gerrit
+ app.kubernetes.io/part-of: receiver
+ app.kubernetes.io/instance: receiver
+ app.kubernetes.io/component: receiver-deployment-receiver
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/managed-by: gerrit-operator
+ app.kubernetes.io/name: gerrit
+ app.kubernetes.io/part-of: receiver
+ app.kubernetes.io/created-by: ReceiverReconciler
+ app.kubernetes.io/instance: receiver
+ app.kubernetes.io/version: unknown
+ app.kubernetes.io/component: receiver-deployment-receiver
+ spec:
+ securityContext:
+ fsGroup: 100
+ imagePullSecrets: []
+ initContainers: []
+ containers:
+ - name: apache-git-http-backend
+ imagePullPolicy: Always
+ image: docker.io/k8sgerrit/apache-git-http-backend:latest
+ env:
+ - name: POD_NAME
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.name
+ ports:
+ - name: http
+ containerPort: 80
+
+ readinessProbe:
+ tcpSocket:
+ port: 80
+
+ livenessProbe:
+ tcpSocket:
+ port: 80
+
+ volumeMounts:
+ - name: shared
+ subPathExpr: "logs/$(POD_NAME)"
+ mountPath: /var/log/apache2
+ - name: apache-credentials
+ mountPath: /var/apache/credentials/.htpasswd
+ subPath: .htpasswd
+ - name: shared
+ subPath: git
+ mountPath: /var/gerrit/git
+ volumes:
+ - name: shared
+ persistentVolumeClaim:
+ claimName: shared-pvc
+ - name: apache-credentials
+ secret:
+ secretName: apache-credentials
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/dependent/service.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/dependent/service.yaml
new file mode 100644
index 0000000..907ee1e
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/dependent/service.yaml
@@ -0,0 +1,25 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: receiver
+ namespace: gerrit
+ labels:
+ app.kubernetes.io/managed-by: gerrit-operator
+ app.kubernetes.io/name: gerrit
+ app.kubernetes.io/part-of: receiver
+ app.kubernetes.io/created-by: ReceiverReconciler
+ app.kubernetes.io/instance: receiver
+ app.kubernetes.io/version: unknown
+ app.kubernetes.io/component: receiver-service
+spec:
+ type: NodePort
+ ports:
+ - name: http
+ port: 80
+ targetPort: 80
+ selector:
+ app.kubernetes.io/managed-by: gerrit-operator
+ app.kubernetes.io/name: gerrit
+ app.kubernetes.io/part-of: receiver
+ app.kubernetes.io/instance: receiver
+ app.kubernetes.io/component: receiver-deployment-receiver
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/receiver.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/receiver.yaml
new file mode 100644
index 0000000..3364ba0
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/receiver.yaml
@@ -0,0 +1,105 @@
+apiVersion: "gerritoperator.google.com/v1alpha6"
+kind: Receiver
+metadata:
+ name: receiver
+ namespace: gerrit
+spec:
+ tolerations:
+ - key: key1
+ operator: Equal
+ value: value1
+ effect: NoSchedule
+
+ affinity:
+ nodeAffinity:
+ requiredDuringSchedulingIgnoredDuringExecution:
+ nodeSelectorTerms:
+ - matchExpressions:
+ - key: disktype
+ operator: In
+ values:
+ - ssd
+
+ topologySpreadConstraints:
+ - maxSkew: 1
+ topologyKey: zone
+ whenUnsatisfiable: DoNotSchedule
+ labelSelector:
+ matchLabels:
+ foo: bar
+
+ priorityClassName: "prio"
+
+ replicas: 1
+ maxSurge: 1
+ maxUnavailable: 1
+
+ resources:
+ requests:
+ cpu: 1
+ memory: 5Gi
+ limits:
+ cpu: 1
+ memory: 6Gi
+
+ readinessProbe:
+ initialDelaySeconds: 0
+ periodSeconds: 10
+ timeoutSeconds: 1
+ successThreshold: 1
+ failureThreshold: 3
+
+ livenessProbe:
+ initialDelaySeconds: 0
+ periodSeconds: 10
+ timeoutSeconds: 1
+ successThreshold: 1
+ failureThreshold: 3
+
+ service:
+ type: NodePort
+ httpPort: 80
+
+ credentialSecretRef: apache-credentials
+
+ containerImages:
+ imagePullSecrets: []
+ imagePullPolicy: Always
+ gerritImages:
+ registry: docker.io
+ org: k8sgerrit
+ tag: latest
+ busyBox:
+ registry: docker.io
+ tag: latest
+
+ storage:
+ storageClasses:
+ readWriteOnce: default
+ readWriteMany: shared-storage
+ nfsWorkaround:
+ enabled: false
+ chownOnStartup: false
+ idmapdConfig: |-
+ [General]
+ Verbosity = 0
+ Domain = localdomain.com
+
+ [Mapping]
+ Nobody-User = nobody
+ Nobody-Group = nogroup
+
+ sharedStorage:
+ externalPVC:
+ enabled: false
+ claimName: ""
+ size: 1Gi
+ volumeName: ""
+ selector:
+ matchLabels:
+ volume-type: ssd
+ aws-availability-zone: us-east-1
+
+ ingress:
+ host: example.com
+ tlsEnabled: false
diff --git a/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/receiver_minimal.yaml b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/receiver_minimal.yaml
new file mode 100644
index 0000000..8fdf155
--- /dev/null
+++ b/charts/k8s-gerrit/operator/src/test/resources/com/google/gerrit/k8s/operator/receiver/receiver_minimal.yaml
@@ -0,0 +1,15 @@
+apiVersion: "gerritoperator.google.com/v1alpha6"
+kind: Receiver
+metadata:
+ name: receiver
+ namespace: gerrit
+spec:
+ credentialSecretRef: apache-credentials
+
+ storage:
+ storageClasses:
+ readWriteOnce: default
+ readWriteMany: shared-storage
+
+ sharedStorage:
+ size: 1Gi