[Operator] Route SSH requests by port

The routing of SSH requests with multiple Gerrit instances was not
working as intended. The subdomain was supposed to be used, but the
VirtualService does not provide matchers for that purpose.

There are two solutions, use the port instead or deploy a gateway for
each Gerrit. To keep the number of managed resources small, the
option of using ports was chosen here.

Change-Id: I0e7832e776498c1d428a46db123f969d5acb17cf
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 46b5fad..7ca5851 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
@@ -31,9 +31,7 @@
 import com.google.gerrit.k8s.operator.cluster.dependent.GerritClusterIstioGateway;
 import com.google.gerrit.k8s.operator.cluster.dependent.GerritIstioCondition;
 import com.google.gerrit.k8s.operator.cluster.dependent.GerritIstioDestinationRule;
-import com.google.gerrit.k8s.operator.cluster.dependent.GerritIstioSshCondition;
 import com.google.gerrit.k8s.operator.cluster.dependent.GerritIstioVirtualService;
-import com.google.gerrit.k8s.operator.cluster.dependent.GerritIstioVirtualServiceSSH;
 import com.google.gerrit.k8s.operator.cluster.dependent.GerritLogsPVC;
 import com.google.gerrit.k8s.operator.cluster.dependent.GitRepositoriesPVC;
 import com.google.gerrit.k8s.operator.cluster.dependent.NfsIdmapdConfigMap;
@@ -114,12 +112,6 @@
           dependsOn = {"gerrit-istio-gateway", "gerrits"},
           useEventSourceWithName = ISTIO_VIRTUAL_SERVICE_EVENT_SOURCE),
       @Dependent(
-          name = "gerrit-istio-virtual-service-ssh",
-          type = GerritIstioVirtualServiceSSH.class,
-          reconcilePrecondition = GerritIstioSshCondition.class,
-          dependsOn = {"gerrit-istio-gateway", "gerrits"},
-          useEventSourceWithName = ISTIO_VIRTUAL_SERVICE_EVENT_SOURCE),
-      @Dependent(
           name = "gerrit-ingress",
           type = GerritClusterIngress.class,
           reconcilePrecondition = GerritClusterIngressCondition.class),
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritClusterIstioGateway.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritClusterIstioGateway.java
index 661dd25..5ad2a0d 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritClusterIstioGateway.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritClusterIstioGateway.java
@@ -84,23 +84,20 @@
     }
 
     List<Gerrit> gerrits = client.resources(Gerrit.class).list().getItems();
-    List<String> sshHostnames = new ArrayList<>();
     if (!gerrits.isEmpty()) {
-      sshHostnames.add(gerritClusterHost);
       for (Gerrit gerrit : gerrits) {
         if (gerrit.getSpec().getService().isSshEnabled()) {
-          sshHostnames.add(gerrit.getMetadata().getName() + "." + gerritClusterHost);
+          servers.add(
+              new ServerBuilder()
+                  .withNewPort()
+                  .withName("ssh-" + gerrit.getMetadata().getName())
+                  .withNumber(gerrit.getSpec().getService().getSshPort())
+                  .withProtocol("TCP")
+                  .endPort()
+                  .withHosts(gerritClusterHost)
+                  .build());
         }
       }
-      servers.add(
-          new ServerBuilder()
-              .withNewPort()
-              .withName("ssh")
-              .withNumber(29418)
-              .withProtocol("TCP")
-              .endPort()
-              .withHosts(sshHostnames)
-              .build());
     }
 
     return servers;
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioSshCondition.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioSshCondition.java
deleted file mode 100644
index 2d45838..0000000
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioSshCondition.java
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.k8s.operator.cluster.dependent;
-
-import com.google.gerrit.k8s.operator.cluster.model.GerritCluster;
-import com.google.gerrit.k8s.operator.cluster.model.GerritClusterIngressConfig.IngressType;
-import io.fabric8.istio.api.networking.v1beta1.VirtualService;
-import io.javaoperatorsdk.operator.api.reconciler.Context;
-import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
-import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
-
-public class GerritIstioSshCondition implements Condition<VirtualService, GerritCluster> {
-
-  @Override
-  public boolean isMet(
-      DependentResource<VirtualService, GerritCluster> dependentResource,
-      GerritCluster gerritCluster,
-      Context<GerritCluster> context) {
-    return gerritCluster.getSpec().getIngress().isEnabled()
-        && gerritCluster.getSpec().getIngress().getType() == IngressType.ISTIO
-        && gerritCluster.getSpec().getGerrits().stream()
-            .anyMatch(g -> g.getSpec().getService().isSshEnabled());
-  }
-}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioVirtualService.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioVirtualService.java
index 68dc4f2..35fd42c 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioVirtualService.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioVirtualService.java
@@ -29,7 +29,12 @@
 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;
@@ -39,7 +44,7 @@
 import java.util.List;
 import java.util.Map;
 
-@KubernetesDependent(resourceDiscriminator = GerritIstioVirtualServiceDiscriminator.class)
+@KubernetesDependent
 public class GerritIstioVirtualService
     extends CRUDKubernetesDependentResource<VirtualService, GerritCluster> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -68,6 +73,7 @@
         .withHosts(gerritClusterHost)
         .withGateways(GerritClusterIstioGateway.NAME)
         .withHttp(getHTTPRoutes(gerritCluster))
+        .withTcp(getTCPRoutes(gerritCluster))
         .endSpec()
         .build();
   }
@@ -187,4 +193,34 @@
             .build());
     return matches;
   }
+
+  private List<TCPRoute> getTCPRoutes(GerritCluster gerritCluster) {
+    List<TCPRoute> routes = new ArrayList<>();
+    for (GerritTemplate gerrit : gerritCluster.getSpec().getGerrits()) {
+      if (gerrit.getSpec().getService().isSshEnabled()) {
+        routes.add(
+            new TCPRouteBuilder()
+                .withMatch(
+                    List.of(
+                        new L4MatchAttributesBuilder()
+                            .withPort(gerrit.getSpec().getService().getSshPort())
+                            .build()))
+                .withRoute(getGerritTCPDestination(gerrit, gerritCluster))
+                .build());
+      }
+    }
+    return routes;
+  }
+
+  private RouteDestination getGerritTCPDestination(
+      GerritTemplate gerrit, GerritCluster gerritCluster) {
+    return new RouteDestinationBuilder()
+        .withNewDestination()
+        .withHost(GerritService.getHostname(gerrit.toGerrit(gerritCluster)))
+        .withNewPort()
+        .withNumber(gerrit.getSpec().getService().getSshPort())
+        .endPort()
+        .endDestination()
+        .build();
+  }
 }
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioVirtualServiceDiscriminator.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioVirtualServiceDiscriminator.java
deleted file mode 100644
index 9405379..0000000
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioVirtualServiceDiscriminator.java
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.k8s.operator.cluster.dependent;
-
-import static com.google.gerrit.k8s.operator.cluster.GerritClusterReconciler.ISTIO_VIRTUAL_SERVICE_EVENT_SOURCE;
-
-import com.google.gerrit.k8s.operator.cluster.model.GerritCluster;
-import io.fabric8.istio.api.networking.v1beta1.VirtualService;
-import io.javaoperatorsdk.operator.api.reconciler.Context;
-import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
-import io.javaoperatorsdk.operator.processing.event.ResourceID;
-import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
-import java.util.Optional;
-
-public class GerritIstioVirtualServiceDiscriminator
-    implements ResourceDiscriminator<VirtualService, GerritCluster> {
-  @Override
-  public Optional<VirtualService> distinguish(
-      Class<VirtualService> resource, GerritCluster gerritCluster, Context<GerritCluster> context) {
-    InformerEventSource<VirtualService, GerritCluster> ies =
-        (InformerEventSource<VirtualService, GerritCluster>)
-            context
-                .eventSourceRetriever()
-                .getResourceEventSourceFor(
-                    VirtualService.class, ISTIO_VIRTUAL_SERVICE_EVENT_SOURCE);
-
-    return ies.get(
-        new ResourceID(
-            gerritCluster.getDependentResourceName(GerritIstioVirtualService.NAME_SUFFIX),
-            gerritCluster.getMetadata().getNamespace()));
-  }
-}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioVirtualServiceSSH.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioVirtualServiceSSH.java
deleted file mode 100644
index 746f705..0000000
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioVirtualServiceSSH.java
+++ /dev/null
@@ -1,96 +0,0 @@
-// Copyright (C) 2022 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.k8s.operator.cluster.dependent;
-
-import com.google.gerrit.k8s.operator.cluster.model.GerritCluster;
-import com.google.gerrit.k8s.operator.gerrit.dependent.GerritService;
-import com.google.gerrit.k8s.operator.gerrit.model.GerritTemplate;
-import io.fabric8.istio.api.networking.v1beta1.L4MatchAttributesBuilder;
-import io.fabric8.istio.api.networking.v1beta1.RouteDestination;
-import io.fabric8.istio.api.networking.v1beta1.RouteDestinationBuilder;
-import io.fabric8.istio.api.networking.v1beta1.TCPRoute;
-import io.fabric8.istio.api.networking.v1beta1.TCPRouteBuilder;
-import io.fabric8.istio.api.networking.v1beta1.VirtualService;
-import io.fabric8.istio.api.networking.v1beta1.VirtualServiceBuilder;
-import io.javaoperatorsdk.operator.api.reconciler.Context;
-import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
-import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.stream.Collectors;
-
-@KubernetesDependent(resourceDiscriminator = GerritIstioVirtualServiceSSHDiscriminator.class)
-public class GerritIstioVirtualServiceSSH
-    extends CRUDKubernetesDependentResource<VirtualService, GerritCluster> {
-  public static final String NAME_SUFFIX = "gerrit-ssh-virtual-service";
-
-  public GerritIstioVirtualServiceSSH() {
-    super(VirtualService.class);
-  }
-
-  @Override
-  protected VirtualService desired(GerritCluster gerritCluster, Context<GerritCluster> context) {
-    String gerritClusterHost = gerritCluster.getSpec().getIngress().getHost();
-    List<GerritTemplate> gerrits = gerritCluster.getSpec().getGerrits();
-
-    return new VirtualServiceBuilder()
-        .withNewMetadata()
-        .withName(gerritCluster.getDependentResourceName(NAME_SUFFIX))
-        .withNamespace(gerritCluster.getMetadata().getNamespace())
-        .withLabels(
-            gerritCluster.getLabels(
-                gerritCluster.getDependentResourceName(NAME_SUFFIX),
-                this.getClass().getSimpleName()))
-        .endMetadata()
-        .withNewSpec()
-        .withHosts(collectHosts(gerrits, gerritClusterHost))
-        .withGateways(GerritClusterIstioGateway.NAME)
-        .withTcp(getTCPRoutes(gerrits, gerritCluster))
-        .endSpec()
-        .build();
-  }
-
-  private List<String> collectHosts(List<GerritTemplate> gerrits, String gerritClusterHost) {
-    return gerrits.stream()
-        .map(g -> g.getMetadata().getName() + "." + gerritClusterHost)
-        .collect(Collectors.toList());
-  }
-
-  private List<TCPRoute> getTCPRoutes(List<GerritTemplate> gerrits, GerritCluster gerritCluster) {
-    List<TCPRoute> routes = new ArrayList<>();
-    for (GerritTemplate gerrit : gerrits) {
-      if (gerrit.getSpec().getService().isSshEnabled()) {
-        routes.add(
-            new TCPRouteBuilder()
-                .withMatch(List.of(new L4MatchAttributesBuilder().withPort(29418).build()))
-                .withRoute(getGerritTCPDestination(gerrit, gerritCluster))
-                .build());
-      }
-    }
-    return routes;
-  }
-
-  private RouteDestination getGerritTCPDestination(
-      GerritTemplate gerrit, GerritCluster gerritCluster) {
-    return new RouteDestinationBuilder()
-        .withNewDestination()
-        .withHost(GerritService.getHostname(gerrit.toGerrit(gerritCluster)))
-        .withNewPort()
-        .withNumber(gerrit.getSpec().getService().getSshPort())
-        .endPort()
-        .endDestination()
-        .build();
-  }
-}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioVirtualServiceSSHDiscriminator.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioVirtualServiceSSHDiscriminator.java
deleted file mode 100644
index 0237125..0000000
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/dependent/GerritIstioVirtualServiceSSHDiscriminator.java
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.k8s.operator.cluster.dependent;
-
-import static com.google.gerrit.k8s.operator.cluster.GerritClusterReconciler.ISTIO_VIRTUAL_SERVICE_EVENT_SOURCE;
-
-import com.google.gerrit.k8s.operator.cluster.model.GerritCluster;
-import io.fabric8.istio.api.networking.v1beta1.VirtualService;
-import io.javaoperatorsdk.operator.api.reconciler.Context;
-import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
-import io.javaoperatorsdk.operator.processing.event.ResourceID;
-import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
-import java.util.Optional;
-
-public class GerritIstioVirtualServiceSSHDiscriminator
-    implements ResourceDiscriminator<VirtualService, GerritCluster> {
-  @Override
-  public Optional<VirtualService> distinguish(
-      Class<VirtualService> resource, GerritCluster gerritCluster, Context<GerritCluster> context) {
-    InformerEventSource<VirtualService, GerritCluster> ies =
-        (InformerEventSource<VirtualService, GerritCluster>)
-            context
-                .eventSourceRetriever()
-                .getResourceEventSourceFor(
-                    VirtualService.class, ISTIO_VIRTUAL_SERVICE_EVENT_SOURCE);
-
-    return ies.get(
-        new ResourceID(
-            gerritCluster.getDependentResourceName(GerritIstioVirtualServiceSSH.NAME_SUFFIX),
-            gerritCluster.getMetadata().getNamespace()));
-  }
-}