Merge "Fix broken table in gerrit chart README"
diff --git a/operator/README.md b/operator/README.md
index 9ce7d15..a659108 100644
--- a/operator/README.md
+++ b/operator/README.md
@@ -5,10 +5,15 @@
 To build all components of the operator run:
 
 ```sh
-# With E2E tests
-mvn clean install jib:dockerBuild
-# Without E2E tests
-mvn clean install -DskipTests jib:dockerBuild
+mvn clean install
+```
+
+## Publish
+
+To publish the container image of the Gerrit Operator run:
+
+```sh
+mvn clean install -P publish
 ```
 
 ## Tests
@@ -54,8 +59,17 @@
 - `gerritPwd`: The password of `gerritUser`
 
 The properties should be set in the `test.properties` file. Alternatively, a
-path of a properties file can be configured by using
-`mvn clean install -Dproperties=<path to properties file> $TARGET`
+path of a properties file can be configured by using the
+`-Dproperties=<path to properties file>`-option.
+
+To run all E2E tests, use:
+
+```sh
+mvn clean install -P integration-test -Dproperties=<path to properties file>
+```
+
+Note, that running the E2E tests will also involve pushing the container image
+to the repository configured in the properties file.
 
 ## Deploy
 
@@ -68,6 +82,24 @@
 Note that these do not include the -v1beta1.yaml files, as those are for old
 Kubernetes versions.
 
+The operator requires a Java Keystore with a keypair inside to allow TLS
+verification for Kubernetes Admission Webhooks. To create a keystore and
+encode it with base64, run:
+
+```sh
+keytool \
+  -genkeypair \
+  -alias operator \
+  -keystore keystore \
+  -keyalg RSA \
+  -keysize 2048 \
+  -validity 3650
+cat keystore | base64 -b 0
+```
+
+Add the result to the Secret in `k8s/operator.yaml` (see comments in the file)
+and also add the base64-encoded password for the keystore to the secret.
+
 Then the operator and associated RBAC rules can be deployed:
 
 ```sh
diff --git a/operator/k8s/operator.yaml b/operator/k8s/operator.yaml
index 47b0bb4..0d43bdb 100644
--- a/operator/k8s/operator.yaml
+++ b/operator/k8s/operator.yaml
@@ -10,6 +10,19 @@
   name: gerrit-operator
   namespace: gerrit-operator
 
+## Required to use an external/persistent keystore, otherwise a keystore using
+## self-signed certificates will be generated
+# ---
+# apiVersion: v1
+# kind: Secret
+# metadata:
+#   name:  gerrit-operator-ssl
+#   namespace: gerrit-operator
+# data:
+#   keystore.jks: # base64-encoded Java keystore
+#   keystore.password: # base64-encoded Java keystore password
+# type: Opaque
+
 ---
 apiVersion: apps/v1
 kind: Deployment
@@ -30,18 +43,34 @@
       - name: operator
         image: k8sgerrit/gerrit-operator
         imagePullPolicy: Always
+        env:
+        - name: NAMESPACE
+          valueFrom:
+            fieldRef:
+              fieldPath: metadata.namespace
         ports:
         - containerPort: 80
         readinessProbe:
           httpGet:
             path: /health
             port: 8080
+            scheme: HTTPS
           initialDelaySeconds: 1
         livenessProbe:
           httpGet:
             path: /health
             port: 8080
+            scheme: HTTPS
           initialDelaySeconds: 30
+      ## Only required, if an external/persistent keystore is being used.
+      #   volumeMounts:
+      #   - name: ssl
+      #     readOnly: true
+      #     mountPath: /operator
+      # volumes:
+      # - name: ssl
+      #   secret:
+      #     secretName: gerrit-operator-ssl
 
 ---
 apiVersion: rbac.authorization.k8s.io/v1
diff --git a/operator/pom.xml b/operator/pom.xml
index 649e89b..6c713bd 100644
--- a/operator/pom.xml
+++ b/operator/pom.xml
@@ -1,5 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 	<modelVersion>4.0.0</modelVersion>
 
 	<groupId>com.google.gerrit.operator</groupId>
@@ -14,10 +16,126 @@
 		<fabric8.version>6.2.0</fabric8.version>
 		<flogger.version>0.7.4</flogger.version>
 		<javaoperatorsdk.version>4.2.4</javaoperatorsdk.version>
+		<jetty.version>11.0.15</jetty.version>
 		<maven.compiler.source>11</maven.compiler.source>
 		<maven.compiler.target>11</maven.compiler.target>
+		<docker.registry>docker.io</docker.registry>
+		<docker.org>k8sgerrit</docker.org>
+
+		<test.docker.registry>docker.io</test.docker.registry>
+		<test.docker.org>k8sgerritdev</test.docker.org>
 	</properties>
 
+	<profiles>
+		<profile>
+			<id>publish</id>
+			<build>
+				<plugins>
+					<plugin>
+						<groupId>com.google.cloud.tools</groupId>
+						<artifactId>jib-maven-plugin</artifactId>
+						<version>3.3.1</version>
+						<executions>
+							<execution>
+								<phase>package</phase>
+								<goals>
+									<goal>build</goal>
+								</goals>
+								<configuration>
+									<container>
+										<mainClass>com.google.gerrit.k8s.operator.Main</mainClass>
+									</container>
+									<containerizingMode>packaged</containerizingMode>
+									<from>
+										<image>gcr.io/distroless/java:11</image>
+									</from>
+									<to>
+										<image>${docker.registry}/${docker.org}/gerrit-operator</image>
+										<tags>
+											<tag>${project.version}</tag>
+										</tags>
+									</to>
+								</configuration>
+							</execution>
+						</executions>
+					</plugin>
+				</plugins>
+			</build>
+		</profile>
+		<profile>
+			<id>integration-test</id>
+			<build>
+				<plugins>
+					<plugin>
+						<groupId>org.codehaus.mojo</groupId>
+						<artifactId>properties-maven-plugin</artifactId>
+						<version>1.1.0</version>
+						<executions>
+							<execution>
+								<phase>initialize</phase>
+								<goals>
+									<goal>read-project-properties</goal>
+								</goals>
+								<configuration>
+									<files>
+										<file>${basedir}/test.properties</file>
+									</files>
+								</configuration>
+							</execution>
+						</executions>
+					</plugin>
+					<plugin>
+						<groupId>com.google.cloud.tools</groupId>
+						<artifactId>jib-maven-plugin</artifactId>
+						<version>3.3.1</version>
+						<executions>
+							<execution>
+								<phase>pre-integration-test</phase>
+								<goals>
+									<goal>build</goal>
+								</goals>
+								<configuration>
+									<container>
+										<mainClass>com.google.gerrit.k8s.operator.Main</mainClass>
+									</container>
+									<containerizingMode>packaged</containerizingMode>
+									<from>
+										<image>gcr.io/distroless/java:11</image>
+									</from>
+									<to>
+										<image>
+											${test.docker.registry}/${test.docker.org}/gerrit-operator</image>
+										<tags>
+											<tag>${project.version}</tag>
+										</tags>
+									</to>
+								</configuration>
+							</execution>
+						</executions>
+					</plugin>
+					<plugin>
+						<artifactId>maven-failsafe-plugin</artifactId>
+						<version>2.22.2</version>
+						<executions>
+							<execution>
+								<phase>integration-test</phase>
+								<goals>
+									<goal>integration-test</goal>
+									<goal>verify</goal>
+								</goals>
+								<configuration>
+									<includes>
+										<include>**/*E2E.java</include>
+									</includes>
+								</configuration>
+							</execution>
+						</executions>
+					</plugin>
+				</plugins>
+			</build>
+		</profile>
+	</profiles>
+
 	<dependencies>
 		<dependency>
 			<groupId>io.javaoperatorsdk</groupId>
@@ -41,9 +159,9 @@
 			<version>${fabric8.version}</version>
 		</dependency>
 		<dependency>
-		    <groupId>io.fabric8</groupId>
-		    <artifactId>istio-client</artifactId>
-		    <version>${fabric8.version}</version>
+			<groupId>io.fabric8</groupId>
+			<artifactId>istio-client</artifactId>
+			<version>${fabric8.version}</version>
 		</dependency>
 		<dependency>
 			<groupId>io.fabric8</groupId>
@@ -52,9 +170,14 @@
 			<scope>provided</scope>
 		</dependency>
 		<dependency>
-			<groupId>org.takes</groupId>
-			<artifactId>takes</artifactId>
-			<version>1.19</version>
+			<groupId>org.eclipse.jetty</groupId>
+			<artifactId>jetty-server</artifactId>
+			<version>${jetty.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.eclipse.jetty</groupId>
+			<artifactId>jetty-servlet</artifactId>
+			<version>${jetty.version}</version>
 		</dependency>
 		<dependency>
 			<groupId>com.google.flogger</groupId>
@@ -67,6 +190,11 @@
 			<version>${flogger.version}</version>
 		</dependency>
 		<dependency>
+			<groupId>com.google.inject</groupId>
+			<artifactId>guice</artifactId>
+			<version>5.1.0</version>
+		</dependency>
+		<dependency>
 			<groupId>org.apache.logging.log4j</groupId>
 			<artifactId>log4j-slf4j-impl</artifactId>
 			<version>2.19.0</version>
@@ -74,7 +202,12 @@
 		<dependency>
 			<groupId>org.eclipse.jgit</groupId>
 			<artifactId>org.eclipse.jgit</artifactId>
-			<version>6.3.0.202209071007-r</version>
+			<version>6.5.0.202303070854-r</version>
+		</dependency>
+		<dependency>
+			<groupId>org.bouncycastle</groupId>
+			<artifactId>bcpkix-jdk18on</artifactId>
+			<version>1.73</version>
 		</dependency>
 		<dependency>
 			<groupId>com.urswolfer.gerrit.client.rest</groupId>
@@ -111,7 +244,7 @@
 				<configuration>
 					<archive>
 						<manifest>
-							<mainClass>com.google.gerrit.k8s.operator.GerritOperator</mainClass>
+							<mainClass>com.google.gerrit.k8s.operator.Main</mainClass>
 							<addDefaultImplementationEntries>
 								true
 							</addDefaultImplementationEntries>
@@ -123,18 +256,26 @@
 				<groupId>com.google.cloud.tools</groupId>
 				<artifactId>jib-maven-plugin</artifactId>
 				<version>3.3.1</version>
-				<configuration>
-					<container>
-						<mainClass>com.google.gerrit.k8s.operator.GerritOperator</mainClass>
-					</container>
-					<containerizingMode>packaged</containerizingMode>
-					<from>
-						<image>gcr.io/distroless/java:11</image>
-					</from>
-					<to>
-						<image>gerrit-operator</image>
-					</to>
-				</configuration>
+				<executions>
+					<execution>
+						<phase>package</phase>
+						<goals>
+							<goal>dockerBuild</goal>
+						</goals>
+						<configuration>
+							<container>
+								<mainClass>com.google.gerrit.k8s.operator.Main</mainClass>
+							</container>
+							<containerizingMode>packaged</containerizingMode>
+							<from>
+								<image>gcr.io/distroless/java:11</image>
+							</from>
+							<to>
+								<image>gerrit-operator</image>
+							</to>
+						</configuration>
+					</execution>
+				</executions>
 			</plugin>
 			<plugin>
 				<groupId>org.apache.maven.plugins</groupId>
@@ -147,7 +288,6 @@
 				<version>2.22.2</version>
 				<configuration>
 					<includes>
-						<include>**/*E2E.java</include>
 						<include>**/*Test.java</include>
 					</includes>
 					<rerunFailingTestsCount>1</rerunFailingTestsCount>
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/EnvModule.java b/operator/src/main/java/com/google/gerrit/k8s/operator/EnvModule.java
new file mode 100644
index 0000000..c9d3739
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/EnvModule.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;
+
+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"));
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/GerritOperator.java b/operator/src/main/java/com/google/gerrit/k8s/operator/GerritOperator.java
index c630846..6f9be7e 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/GerritOperator.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/GerritOperator.java
@@ -14,40 +14,98 @@
 
 package com.google.gerrit.k8s.operator;
 
-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.receiver.ReceiverReconciler;
-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.Operator;
-import java.io.IOException;
-import org.takes.facets.fork.FkRegex;
-import org.takes.facets.fork.TkFork;
-import org.takes.http.Exit;
-import org.takes.http.FtBasic;
+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;
 
-  public static void main(String[] args) throws IOException {
-    Config config = new ConfigBuilder().withNamespace(null).build();
-    KubernetesClient client = new KubernetesClientBuilder().withConfig(config).build();
-    Operator operator = new Operator(client);
-    logger.atFine().log("Registering GerritCluster Reconciler");
-    operator.register(new GerritClusterReconciler(client));
-    logger.atFine().log("Registering GitGc Reconciler");
-    operator.register(new GitGarbageCollectionReconciler(client));
-    logger.atFine().log("Registering Gerrit Reconciler");
-    operator.register(new GerritReconciler(client));
-    logger.atFine().log("Registering Receiver Reconciler");
-    operator.register(new ReceiverReconciler(client));
-    operator.installShutdownHook();
+  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();
+  }
 
-    new FtBasic(new TkFork(new FkRegex("/health", "ALL GOOD.")), 8080).start(Exit.NEVER);
+  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/operator/src/main/java/com/google/gerrit/k8s/operator/LifecycleManager.java b/operator/src/main/java/com/google/gerrit/k8s/operator/LifecycleManager.java
new file mode 100644
index 0000000..8e556a7
--- /dev/null
+++ b/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/operator/src/main/java/com/google/gerrit/k8s/operator/Main.java b/operator/src/main/java/com/google/gerrit/k8s/operator/Main.java
new file mode 100644
index 0000000..cfba467
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/Main.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;
+
+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(GerritOperator.class).start();
+    injector.getInstance(HttpServer.class).start();
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/OperatorModule.java b/operator/src/main/java/com/google/gerrit/k8s/operator/OperatorModule.java
new file mode 100644
index 0000000..4e78219
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/OperatorModule.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;
+
+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.receiver.ReceiverReconciler;
+import com.google.gerrit.k8s.operator.server.ServerModule;
+import com.google.inject.AbstractModule;
+import com.google.inject.multibindings.Multibinder;
+import com.google.inject.name.Names;
+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(String.class)
+        .annotatedWith(Names.named("Namespace"))
+        .toInstance(System.getenv("NAMESPACE"));
+    bind(KubernetesClient.class).toInstance(getKubernetesClient());
+    bind(LifecycleManager.class);
+    bind(GerritOperator.class);
+    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);
+  }
+
+  private KubernetesClient getKubernetesClient() {
+    Config config = new ConfigBuilder().withNamespace(null).build();
+    return new KubernetesClientBuilder().withConfig(config).build();
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/GerritClusterReconciler.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/GerritClusterReconciler.java
index 48a973c..a9267ce 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/GerritClusterReconciler.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/GerritClusterReconciler.java
@@ -18,6 +18,8 @@
 
 import com.google.gerrit.k8s.operator.gerrit.Gerrit;
 import com.google.gerrit.k8s.operator.receiver.Receiver;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
 import io.fabric8.kubernetes.api.model.PersistentVolumeClaim;
 import io.fabric8.kubernetes.client.KubernetesClient;
 import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
@@ -38,6 +40,7 @@
 import java.util.Map;
 import java.util.stream.Collectors;
 
+@Singleton
 @ControllerConfiguration(
     dependents = {
       @Dependent(type = GitRepositoriesPVC.class, useEventSourceWithName = PVC_EVENT_SOURCE),
@@ -56,19 +59,20 @@
   private static final String GERRIT_INGRESS_EVENT_SOURCE = "gerrit-ingress";
   private static final String GERRIT_ISTIO_EVENT_SOURCE = "gerrit-istio";
 
-  private final KubernetesClient kubernetesClient;
+  private final KubernetesClient client;
 
   private GerritIngress gerritIngress;
   private GerritIstioGateway gerritIstioGateway;
 
+  @Inject
   public GerritClusterReconciler(KubernetesClient client) {
-    this.kubernetesClient = client;
+    this.client = client;
 
     this.gerritIngress = new GerritIngress();
-    this.gerritIngress.setKubernetesClient(kubernetesClient);
+    this.gerritIngress.setKubernetesClient(client);
 
     this.gerritIstioGateway = new GerritIstioGateway();
-    this.gerritIstioGateway.setKubernetesClient(kubernetesClient);
+    this.gerritIstioGateway.setKubernetesClient(client);
   }
 
   @Override
@@ -158,7 +162,7 @@
   private List<String> getManagedMemberInstances(
       GerritCluster gerritCluster,
       Class<? extends GerritClusterMember<? extends GerritClusterMemberSpec, ?>> clazz) {
-    return kubernetesClient
+    return client
         .resources(clazz)
         .inNamespace(gerritCluster.getMetadata().getNamespace())
         .list()
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/GerritReconciler.java b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/GerritReconciler.java
index 96238a2..00d80cd 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/GerritReconciler.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/GerritReconciler.java
@@ -19,6 +19,8 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.k8s.operator.cluster.GerritCluster;
 import com.google.gerrit.k8s.operator.cluster.GerritIngressConfig.IngressType;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
 import io.fabric8.kubernetes.api.model.ConfigMap;
 import io.fabric8.kubernetes.api.model.Secret;
 import io.fabric8.kubernetes.client.KubernetesClient;
@@ -44,6 +46,7 @@
 import java.util.Optional;
 import java.util.stream.Collectors;
 
+@Singleton
 @ControllerConfiguration(
     dependents = {
       @Dependent(
@@ -73,6 +76,7 @@
   private final GerritIstioVirtualService virtualService;
   private final GerritIstioDestinationRule destinationRule;
 
+  @Inject
   public GerritReconciler(KubernetesClient client) {
     this.client = client;
 
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollectionReconciler.java b/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollectionReconciler.java
index 45bd3c1..9fc7012 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollectionReconciler.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollectionReconciler.java
@@ -17,6 +17,8 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.k8s.operator.cluster.GerritCluster;
 import com.google.gerrit.k8s.operator.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;
@@ -38,20 +40,22 @@
 import java.util.Set;
 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 kubernetesClient;
+  private final KubernetesClient client;
 
   private GitGarbageCollectionCronJob dependentCronJob;
 
-  public GitGarbageCollectionReconciler(KubernetesClient kubernetesClient) {
-    this.kubernetesClient = kubernetesClient;
+  @Inject
+  public GitGarbageCollectionReconciler(KubernetesClient client) {
+    this.client = client;
     this.dependentCronJob = new GitGarbageCollectionCronJob();
-    this.dependentCronJob.setKubernetesClient(kubernetesClient);
+    this.dependentCronJob.setKubernetesClient(client);
   }
 
   @Override
@@ -116,7 +120,7 @@
 
   private GitGarbageCollection excludeProjectsHandledSeparately(GitGarbageCollection currentGitGc) {
     List<GitGarbageCollection> gitGcs =
-        kubernetesClient
+        client
             .resources(GitGarbageCollection.class)
             .inNamespace(currentGitGc.getMetadata().getNamespace())
             .list()
@@ -134,7 +138,7 @@
 
   private void validateGitGCProjectList(GitGarbageCollection gitGc) {
     List<GitGarbageCollection> gitGcs =
-        kubernetesClient
+        client
             .resources(GitGarbageCollection.class)
             .inNamespace(gitGc.getMetadata().getNamespace())
             .list()
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/ReceiverReconciler.java b/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/ReceiverReconciler.java
index ba5918a..a8825a4 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/ReceiverReconciler.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/ReceiverReconciler.java
@@ -17,6 +17,8 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.k8s.operator.cluster.GerritCluster;
 import com.google.gerrit.k8s.operator.cluster.GerritIngressConfig.IngressType;
+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;
@@ -34,6 +36,7 @@
 import java.util.Map;
 import java.util.stream.Collectors;
 
+@Singleton
 @ControllerConfiguration(
     dependents = {
       @Dependent(name = "receiver-deployment", type = ReceiverDeploymentDependentResource.class),
@@ -49,6 +52,7 @@
 
   private final ReceiverIstioVirtualService virtualService;
 
+  @Inject
   public ReceiverReconciler(KubernetesClient client) {
     this.client = client;
 
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/server/FileSystemKeyStoreProvider.java b/operator/src/main/java/com/google/gerrit/k8s/operator/server/FileSystemKeyStoreProvider.java
new file mode 100644
index 0000000..fe2ef88
--- /dev/null
+++ b/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 implements KeyStoreProvider {
+  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/operator/src/main/java/com/google/gerrit/k8s/operator/server/GeneratedKeyStoreProvider.java b/operator/src/main/java/com/google/gerrit/k8s/operator/server/GeneratedKeyStoreProvider.java
new file mode 100644
index 0000000..ba4f7c4
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/server/GeneratedKeyStoreProvider.java
@@ -0,0 +1,133 @@
+// 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 implements KeyStoreProvider {
+  private static final Path KEYSTORE_PATH = Path.of("/tmp/keystore.jks");
+  private static final String ALIAS = "operator";
+
+  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(ALIAS, 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/operator/src/main/java/com/google/gerrit/k8s/operator/server/HealthcheckServlet.java b/operator/src/main/java/com/google/gerrit/k8s/operator/server/HealthcheckServlet.java
new file mode 100644
index 0000000..6097bde
--- /dev/null
+++ b/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/operator/src/main/java/com/google/gerrit/k8s/operator/server/HttpServer.java b/operator/src/main/java/com/google/gerrit/k8s/operator/server/HttpServer.java
new file mode 100644
index 0000000..10956d8
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/server/HttpServer.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.server;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+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.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;
+
+  @Inject
+  public HttpServer(KeyStoreProvider keyStoreProvider) {
+    this.keyStoreProvider = keyStoreProvider;
+  }
+
+  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();
+    servletHandler.addServletWithMapping(HealthcheckServlet.class, "/health");
+    server.setHandler(servletHandler);
+
+    server.start();
+  }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/server/KeyStoreProvider.java b/operator/src/main/java/com/google/gerrit/k8s/operator/server/KeyStoreProvider.java
new file mode 100644
index 0000000..de2cf62
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/server/KeyStoreProvider.java
@@ -0,0 +1,24 @@
+// 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;
+
+public interface KeyStoreProvider {
+  Path getKeyStorePath();
+
+  String getKeyStorePassword() throws IOException;
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/server/ServerModule.java b/operator/src/main/java/com/google/gerrit/k8s/operator/server/ServerModule.java
new file mode 100644
index 0000000..cc1a3aa
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/server/ServerModule.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.server;
+
+import static com.google.gerrit.k8s.operator.server.FileSystemKeyStoreProvider.KEYSTORE_PATH;
+
+import com.google.inject.AbstractModule;
+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);
+  }
+}