blob: bed92db575dcee2ac40042fbfb240d5b600219ad [file] [log] [blame]
// 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.network.Constants.GERRIT_FORBIDDEN_URL_PATTERN;
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 static com.google.gerrit.k8s.operator.network.Constants.UPLOAD_PACK_URL_PATTERN;
import com.google.gerrit.k8s.operator.api.model.cluster.GerritCluster;
import com.google.gerrit.k8s.operator.api.model.network.GerritNetwork;
import com.google.gerrit.k8s.operator.gerrit.dependent.GerritService;
import com.google.gerrit.k8s.operator.receiver.dependent.ReceiverService;
import com.google.gerrit.k8s.operator.util.CRUDReconcileAddKubernetesDependentResource;
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.KubernetesDependent;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@KubernetesDependent
public class GerritClusterIngress
extends CRUDReconcileAddKubernetesDependentResource<Ingress, GerritNetwork> {
public static final String INGRESS_NAME = "gerrit-ingress";
public static final String SESSION_COOKIE_NAME = "Gerrit_Session";
public static final Duration SESSION_COOKIE_TTL = Duration.ofSeconds(3600L);
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");
StringBuilder configSnippet = new StringBuilder();
if (gerritNetwork.hasPrimaryGerrit()) {
configSnippet = createNginxConfigSnippetForbidHARoutes(configSnippet);
}
if (gerritNetwork.hasPrimaryGerrit() && gerritNetwork.hasGerritReplica()) {
String svcName = GerritService.getName(gerritNetwork.getSpec().getGerritReplica().getName());
configSnippet =
createNginxConfigSnippetGerritReplicaRouting(
configSnippet,
"service=git-upload-pack",
gerritNetwork.getMetadata().getNamespace(),
svcName);
}
if (gerritNetwork.hasReceiver() && gerritNetwork.hasGerritReplica()) {
String svcName = ReceiverService.getName(gerritNetwork.getSpec().getReceiver().getName());
configSnippet =
createNginxConfigSnippetGerritReplicaRouting(
configSnippet,
"service=git-receive-pack",
gerritNetwork.getMetadata().getNamespace(),
svcName);
}
if (configSnippet.length() > 0) {
annotations.put(
"nginx.ingress.kubernetes.io/configuration-snippet", configSnippet.toString().trim());
}
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",
String.valueOf(SESSION_COOKIE_TTL.getSeconds()));
annotations.put(
"nginx.ingress.kubernetes.io/session-cookie-expires",
String.valueOf(SESSION_COOKIE_TTL.getSeconds()));
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 StringBuilder createNginxConfigSnippetGerritReplicaRouting(
StringBuilder configSnippet, String queryParam, String namespace, String svcName) {
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}\n");
return configSnippet;
}
private StringBuilder createNginxConfigSnippetForbidHARoutes(StringBuilder configSnippet) {
configSnippet.append("location ~ ");
configSnippet.append(GERRIT_FORBIDDEN_URL_PATTERN);
configSnippet.append(" {\n");
configSnippet.append(" deny all;\n");
configSnippet.append(" return 403;\n");
configSnippet.append("}\n");
return configSnippet;
}
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();
HTTPIngressPathBuilder builder =
new HTTPIngressPathBuilder()
.withPathType("Prefix")
.withNewBackend()
.withNewService()
.withName(svcName)
.withPort(port)
.endService()
.endBackend();
if (gerritNetwork.hasGerritReplica()) {
for (String path : List.of(PROJECTS_URL_PATTERN, RECEIVE_PACK_URL_PATTERN)) {
paths.add(builder.withPath(path).build());
}
} else {
paths.add(builder.withPath("/").build());
}
return paths;
}
}