Merge "[operator] Add Receiver CustomResource"
diff --git a/operator/README.md b/operator/README.md
index beeb79a..2f7116a 100644
--- a/operator/README.md
+++ b/operator/README.md
@@ -76,6 +76,9 @@
### Gerrit
+**NOTE:** A primary Gerrit should never be installed in the same GerritCluster as a
+Receiver to avoid conflicts when writing into repositories.
+
An example of a Gerrit-CustomResource can be found at `k8s/gerrit.sample.yaml`.
To install it into the cluster run:
@@ -96,6 +99,21 @@
The operator will create a CronJob based on the provided spec.
+### Receiver
+
+**NOTE:** A Receiver should never be installed in the same GerritCluster as a
+primary Gerrit to avoid conflicts when writing into repositories.
+
+An example of a Receiver-CustomResource can be found at `k8s/receiver.sample.yaml`.
+To install it into the cluster run:
+
+```sh
+kubectl apply -f k8s/receiver.sample.yaml
+```
+
+The operator will create all resources to run a receiver for push replication
+requests.
+
## Configure custom resources
### GerritCluster
@@ -450,3 +468,103 @@
# values:
# - ssd
```
+
+### Receiver
+
+```yaml
+apiVersion: "gerritoperator.google.com/v1alpha1"
+kind: Receiver
+metadata:
+ name: receiver
+spec:
+ ## Name of the Gerrit cluster this Gerrit is a part of. (mandatory)
+ cluster: gerrit
+
+ ## Pod tolerations (https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/)
+ ## (optional)
+ tolerations: []
+ # - key: "key1"
+ # operator: "Equal"
+ # value: "value1"
+ # effect: "NoSchedule"
+
+ ## Pod affinity (https://kubernetes.io/docs/tasks/configure-pod-container/assign-pods-nodes-using-node-affinity/)
+ ## (optional)
+ affinity: {}
+ # nodeAffinity:
+ # requiredDuringSchedulingIgnoredDuringExecution:
+ # nodeSelectorTerms:
+ # - matchExpressions:
+ # - key: disktype
+ # operator: In
+ # values:
+ # - ssd
+
+ ## Pod topology spread constraints (https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/#:~:text=You%20can%20use%20topology%20spread,well%20as%20efficient%20resource%20utilization.)
+ ## (optional)
+ topologySpreadConstraints: []
+ # - maxSkew: 1
+ # topologyKey: zone
+ # whenUnsatisfiable: DoNotSchedule
+ # labelSelector:
+ # matchLabels:
+ # foo: bar
+
+ ## PriorityClass to be used with the pod (https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/)
+ ## (optional)
+ priorityClassName: ""
+
+ ## Number of pods running Gerrit in the StatefulSet (default: 1)
+ replicas: 1
+
+ ## Ordinal or percentage of pods that are allowed to be created in addition during
+ ## rolling updates. (default: 1)
+ maxSurge: 1
+
+ ## Ordinal or percentage of pods that are allowed to be unavailable during
+ ## rolling updates. (default: 1)
+ maxUnavailable: 1
+
+ ## Resource requests/limits of the receiver container
+ ## (https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/)
+ ## (optional)
+ resources: {}
+ # requests:
+ # cpu: 1
+ # memory: 5Gi
+ # limits:
+ # cpu: 1
+ # memory: 6Gi
+
+ ## Readiness probe (https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#configure-probes)
+ ## The action will be set by the operator. All other probe parameters can be set.
+ ## (optional)
+ readinessProbe: {}
+ # initialDelaySeconds: 0
+ # periodSeconds: 10
+ # timeoutSeconds: 1
+ # successThreshold: 1
+ # failureThreshold: 3
+
+ ## Liveness probe (https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#configure-probes)
+ ## The action will be set by the operator. All other probe parameters can be set.
+ ## (optional)
+ livenessProbe: {}
+ # initialDelaySeconds: 0
+ # periodSeconds: 10
+ # timeoutSeconds: 1
+ # successThreshold: 1
+ # failureThreshold: 3
+
+ ## Configuration for the service used to manage network access to the StatefulSet
+ service:
+ ## Service type (default: NodePort)
+ type: NodePort
+
+ ## Port used for HTTP requests (default: 80)
+ httpPort: 80
+
+ ## Name of the secret containing the .htpasswd file used to configure basic
+ ## authentication within the Apache server (mandatory)
+ credentialSecretRef: null
+```
diff --git a/operator/k8s/operator.yaml b/operator/k8s/operator.yaml
index 76857aa..9ab055f 100644
--- a/operator/k8s/operator.yaml
+++ b/operator/k8s/operator.yaml
@@ -73,6 +73,7 @@
- "apps"
resources:
- statefulsets
+ - deployments
verbs:
- '*'
- apiGroups:
diff --git a/operator/k8s/receiver.sample.yaml b/operator/k8s/receiver.sample.yaml
new file mode 100644
index 0000000..b992495
--- /dev/null
+++ b/operator/k8s/receiver.sample.yaml
@@ -0,0 +1,17 @@
+apiVersion: "gerritoperator.google.com/v1alpha1"
+kind: Receiver
+metadata:
+ name: receiver
+spec:
+ cluster: gerrit
+ credentialSecretRef: receiver-htpasswd
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: receiver-htpasswd
+ labels:
+ app: gerrit
+data:
+ .htpasswd: Z2l0OiRhcHIxJE8vTGJMS0M3JFE2MEdXRTdPY3FTRU1TZmUvSzh4VS4=
+type: Opaque
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 e28c073..c630846 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
@@ -18,6 +18,7 @@
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;
@@ -42,6 +43,8 @@
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();
operator.start();
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/GerritCluster.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/GerritCluster.java
index 98f6232..1fb8e0e 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/GerritCluster.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/GerritCluster.java
@@ -19,7 +19,6 @@
import static com.google.gerrit.k8s.operator.cluster.NfsIdmapdConfigMap.NFS_IDMAPD_CM_NAME;
import com.fasterxml.jackson.annotation.JsonIgnore;
-import com.google.gerrit.k8s.operator.gerrit.Gerrit;
import io.fabric8.kubernetes.api.model.Container;
import io.fabric8.kubernetes.api.model.ContainerBuilder;
import io.fabric8.kubernetes.api.model.EnvVar;
@@ -134,8 +133,9 @@
}
@JsonIgnore
- public static boolean isGerritInstancePartOfCluster(Gerrit gerrit, GerritCluster cluster) {
- return gerrit.getSpec().getCluster().equals(cluster.getMetadata().getName());
+ public static boolean isMemberPartOfCluster(
+ GerritClusterMemberSpec memberSpec, GerritCluster cluster) {
+ return memberSpec.getCluster().equals(cluster.getMetadata().getName());
}
@JsonIgnore
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/GerritClusterMember.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/GerritClusterMember.java
new file mode 100644
index 0000000..f458e59
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/GerritClusterMember.java
@@ -0,0 +1,23 @@
+// 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 io.fabric8.kubernetes.api.model.Namespaced;
+import io.fabric8.kubernetes.client.CustomResource;
+
+public abstract class GerritClusterMember<S extends GerritClusterMemberSpec, T>
+ extends CustomResource<S, T> implements Namespaced {
+ private static final long serialVersionUID = 1L;
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/GerritClusterMemberSpec.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/GerritClusterMemberSpec.java
new file mode 100644
index 0000000..7fe7d15
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/GerritClusterMemberSpec.java
@@ -0,0 +1,21 @@
+// 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;
+
+public interface GerritClusterMemberSpec {
+ public String getCluster();
+
+ public void setCluster(String cluster);
+}
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 cf177b3..8b02dbe 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
@@ -17,6 +17,7 @@
import static com.google.gerrit.k8s.operator.cluster.GerritClusterReconciler.PVC_EVENT_SOURCE;
import com.google.gerrit.k8s.operator.gerrit.Gerrit;
+import com.google.gerrit.k8s.operator.receiver.Receiver;
import io.fabric8.kubernetes.api.model.PersistentVolumeClaim;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
@@ -31,6 +32,8 @@
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.Collection;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -82,47 +85,72 @@
.build(),
context);
+ final SecondaryToPrimaryMapper<Receiver> receiverMapper =
+ (Receiver receiver) ->
+ context
+ .getPrimaryCache()
+ .list(
+ gerritCluster ->
+ gerritCluster
+ .getMetadata()
+ .getName()
+ .equals(receiver.getSpec().getCluster()))
+ .map(ResourceID::fromResource)
+ .collect(Collectors.toSet());
+
+ InformerEventSource<Receiver, GerritCluster> receiverEventSource =
+ new InformerEventSource<>(
+ InformerConfiguration.from(Receiver.class, context)
+ .withSecondaryToPrimaryMapper(receiverMapper)
+ .build(),
+ context);
+
InformerEventSource<PersistentVolumeClaim, GerritCluster> pvcEventSource =
new InformerEventSource<>(
InformerConfiguration.from(PersistentVolumeClaim.class, context).build(), context);
Map<String, EventSource> eventSources =
- EventSourceInitializer.nameEventSources(gerritEventSource);
+ EventSourceInitializer.nameEventSources(gerritEventSource, receiverEventSource);
eventSources.put(PVC_EVENT_SOURCE, pvcEventSource);
eventSources.put(GERRIT_INGRESS_EVENT_SOURCE, this.gerritIngress.initEventSource(context));
-
return eventSources;
}
@Override
public UpdateControl<GerritCluster> reconcile(
GerritCluster gerritCluster, Context<GerritCluster> context) {
- List<String> managedGerrits = getManagedGerritInstances(gerritCluster);
- if (!managedGerrits.isEmpty() && gerritCluster.getSpec().getIngress().isEnabled()) {
+ Map<String, List<String>> members = new HashMap<>();
+ members.put("gerrit", getManagedMemberInstances(gerritCluster, Gerrit.class));
+ members.put("receiver", getManagedMemberInstances(gerritCluster, Receiver.class));
+ if (members.values().stream().flatMap(Collection::stream).count() > 0
+ && gerritCluster.getSpec().getIngress().isEnabled()) {
this.gerritIngress.reconcile(gerritCluster, context);
}
- return UpdateControl.patchStatus(updateStatus(gerritCluster, managedGerrits));
+ return UpdateControl.patchStatus(updateStatus(gerritCluster, members));
}
- private GerritCluster updateStatus(GerritCluster gerritCluster, List<String> managedGerrits) {
+ private GerritCluster updateStatus(
+ GerritCluster gerritCluster, Map<String, List<String>> members) {
GerritClusterStatus status = gerritCluster.getStatus();
if (status == null) {
status = new GerritClusterStatus();
}
- status.setManagedGerritInstances(managedGerrits);
+ status.setMembers(members);
gerritCluster.setStatus(status);
return gerritCluster;
}
- private List<String> getManagedGerritInstances(GerritCluster gerritCluster) {
+ private List<String> getManagedMemberInstances(
+ GerritCluster gerritCluster,
+ Class<? extends GerritClusterMember<? extends GerritClusterMemberSpec, ?>> clazz) {
return kubernetesClient
- .resources(Gerrit.class)
+ .resources(clazz)
.inNamespace(gerritCluster.getMetadata().getNamespace())
.list()
.getItems()
.stream()
- .filter(gerrit -> GerritCluster.isGerritInstancePartOfCluster(gerrit, gerritCluster))
- .map(gerrit -> gerrit.getMetadata().getName())
+ .filter(c -> GerritCluster.isMemberPartOfCluster(c.getSpec(), gerritCluster))
+ .map(c -> c.getMetadata().getName())
.collect(Collectors.toList());
}
}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/GerritClusterStatus.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/GerritClusterStatus.java
index 5f45a7c..c35b328 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/GerritClusterStatus.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/GerritClusterStatus.java
@@ -15,15 +15,16 @@
package com.google.gerrit.k8s.operator.cluster;
import java.util.List;
+import java.util.Map;
public class GerritClusterStatus {
- private List<String> managedGerritInstances;
+ private Map<String, List<String>> members;
- public List<String> getManagedGerritInstances() {
- return managedGerritInstances;
+ public Map<String, List<String>> getMembers() {
+ return members;
}
- public void setManagedGerritInstances(List<String> managedGerritInstances) {
- this.managedGerritInstances = managedGerritInstances;
+ public void setMembers(Map<String, List<String>> members) {
+ this.members = members;
}
}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/GerritIngress.java b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/GerritIngress.java
index cf3589b..9d3aabd 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/GerritIngress.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/cluster/GerritIngress.java
@@ -14,10 +14,10 @@
package com.google.gerrit.k8s.operator.cluster;
-import static com.google.gerrit.k8s.operator.gerrit.ServiceDependentResource.HTTP_PORT_NAME;
-
import com.google.gerrit.k8s.operator.gerrit.Gerrit;
import com.google.gerrit.k8s.operator.gerrit.ServiceDependentResource;
+import com.google.gerrit.k8s.operator.receiver.Receiver;
+import com.google.gerrit.k8s.operator.receiver.ReceiverServiceDependentResource;
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;
@@ -33,6 +33,7 @@
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
import java.util.ArrayList;
import java.util.List;
+import java.util.Set;
import java.util.stream.Collectors;
@KubernetesDependent(labelSelector = "app.kubernetes.io/component=gerrit-ingress")
@@ -52,9 +53,31 @@
.list()
.getItems()
.stream()
- .filter(gerrit -> GerritCluster.isGerritInstancePartOfCluster(gerrit, gerritCluster))
+ .filter(gerrit -> GerritCluster.isMemberPartOfCluster(gerrit.getSpec(), gerritCluster))
.collect(Collectors.toList());
+ List<Receiver> receivers =
+ client
+ .resources(Receiver.class)
+ .inNamespace(gerritCluster.getMetadata().getNamespace())
+ .list()
+ .getItems()
+ .stream()
+ .filter(r -> GerritCluster.isMemberPartOfCluster(r.getSpec(), gerritCluster))
+ .collect(Collectors.toList());
+
+ List<String> hosts = new ArrayList<>();
+ List<IngressRule> ingressRules = new ArrayList<>();
+ for (Receiver receiver : receivers) {
+ ingressRules.add(getReceiverIngressRule(gerritCluster, receiver));
+ hosts.add(getFullHostname(receiver.getMetadata().getName(), gerritCluster));
+ }
+
+ ingressRules.addAll(getGerritIngressRules(gerritCluster, gerrits));
+ for (Gerrit gerrit : gerrits) {
+ hosts.add(getFullHostname(gerrit.getMetadata().getName(), gerritCluster));
+ }
+
Ingress gerritIngress =
new IngressBuilder()
.withNewMetadata()
@@ -64,20 +87,16 @@
.withAnnotations(gerritCluster.getSpec().getIngress().getAnnotations())
.endMetadata()
.withNewSpec()
- .withTls(getIngressTLS(gerritCluster, gerrits))
- .withRules(getIngressRules(gerritCluster, gerrits))
+ .withTls(getIngressTLS(gerritCluster, hosts))
+ .withRules(ingressRules)
.endSpec()
.build();
return gerritIngress;
}
- private IngressTLS getIngressTLS(GerritCluster gerritCluster, List<Gerrit> gerrits) {
+ private IngressTLS getIngressTLS(GerritCluster gerritCluster, List<String> hosts) {
if (gerritCluster.getSpec().getIngress().getTls().isEnabled()) {
- List<String> hosts = new ArrayList<>();
- for (Gerrit gerrit : gerrits) {
- hosts.add(getFullHostname(ServiceDependentResource.getName(gerrit), gerritCluster));
- }
return new IngressTLSBuilder()
.withHosts(hosts)
.withSecretName(gerritCluster.getSpec().getIngress().getTls().getSecret())
@@ -86,24 +105,36 @@
return new IngressTLS();
}
- private List<IngressRule> getIngressRules(GerritCluster gerritCluster, List<Gerrit> gerrits) {
+ private List<IngressRule> getGerritIngressRules(
+ GerritCluster gerritCluster, List<Gerrit> gerrits) {
List<IngressRule> ingressRules = new ArrayList<>();
for (Gerrit gerrit : gerrits) {
- String svcName = ServiceDependentResource.getName(gerrit);
+ String gerritSvcName = ServiceDependentResource.getName(gerrit);
ingressRules.add(
new IngressRuleBuilder()
- .withHost(getFullHostname(svcName, gerritCluster))
+ .withHost(getFullHostname(gerritSvcName, gerritCluster))
.withNewHttp()
- .withPaths(getHTTPIngressPaths(svcName))
+ .withPaths(getGerritHTTPIngressPath(gerritSvcName))
.endHttp()
.build());
}
+
return ingressRules;
}
- public HTTPIngressPath getHTTPIngressPaths(String svcName) {
- ServiceBackendPort port = new ServiceBackendPortBuilder().withName(HTTP_PORT_NAME).build();
+ private IngressRule getReceiverIngressRule(GerritCluster gerritCluster, Receiver receiver) {
+ return new IngressRuleBuilder()
+ .withHost(getFullHostname(receiver.getMetadata().getName(), gerritCluster))
+ .withNewHttp()
+ .withPaths(getReceiverIngressPaths(ReceiverServiceDependentResource.getName(receiver)))
+ .endHttp()
+ .build();
+ }
+
+ public HTTPIngressPath getGerritHTTPIngressPath(String svcName) {
+ ServiceBackendPort port =
+ new ServiceBackendPortBuilder().withName(ServiceDependentResource.HTTP_PORT_NAME).build();
return new HTTPIngressPathBuilder()
.withPathType("Prefix")
@@ -117,6 +148,29 @@
.build();
}
+ public List<HTTPIngressPath> getReceiverIngressPaths(String svcName) {
+ List<HTTPIngressPath> paths = new ArrayList<>();
+ ServiceBackendPort port =
+ new ServiceBackendPortBuilder()
+ .withName(ReceiverServiceDependentResource.HTTP_PORT_NAME)
+ .build();
+
+ for (String path : Set.of("/a/projects", "/new", "/git")) {
+ paths.add(
+ new HTTPIngressPathBuilder()
+ .withPathType("Prefix")
+ .withPath(path)
+ .withNewBackend()
+ .withNewService()
+ .withName(svcName)
+ .withPort(port)
+ .endService()
+ .endBackend()
+ .build());
+ }
+ return paths;
+ }
+
public static String getFullHostname(String svcName, GerritCluster gerritCluster) {
return String.format("%s.%s", svcName, gerritCluster.getSpec().getIngress().getHost());
}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/Gerrit.java b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/Gerrit.java
index 1ca2cc4..92c676c 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/Gerrit.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/Gerrit.java
@@ -14,8 +14,8 @@
package com.google.gerrit.k8s.operator.gerrit;
+import com.google.gerrit.k8s.operator.cluster.GerritClusterMember;
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;
@@ -25,7 +25,7 @@
@Group("gerritoperator.google.com")
@Version("v1alpha1")
@ShortNames("gcr")
-public class Gerrit extends CustomResource<GerritSpec, GerritStatus> implements Namespaced {
+public class Gerrit extends GerritClusterMember<GerritSpec, GerritStatus> implements Namespaced {
private static final long serialVersionUID = 1L;
public String toString() {
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/GerritConfigMapDependentResource.java b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/GerritConfigMapDependentResource.java
index 829eab8..58cfec4 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/GerritConfigMapDependentResource.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/GerritConfigMapDependentResource.java
@@ -85,7 +85,7 @@
.build();
}
- protected static String getName(Gerrit gerrit) {
+ public static String getName(Gerrit gerrit) {
return String.format("%s-configmap", gerrit.getMetadata().getName());
}
}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/GerritInitConfigMapDependentResource.java b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/GerritInitConfigMapDependentResource.java
index c52cd82..ab21184 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/GerritInitConfigMapDependentResource.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/GerritInitConfigMapDependentResource.java
@@ -91,7 +91,7 @@
}
}
- protected static String getName(Gerrit gerrit) {
+ public static String getName(Gerrit gerrit) {
return String.format("%s-init-configmap", gerrit.getMetadata().getName());
}
}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/GerritSpec.java b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/GerritSpec.java
index 56643b8..e3311dd 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/GerritSpec.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/gerrit/GerritSpec.java
@@ -14,6 +14,7 @@
package com.google.gerrit.k8s.operator.gerrit;
+import com.google.gerrit.k8s.operator.cluster.GerritClusterMemberSpec;
import io.fabric8.kubernetes.api.model.Affinity;
import io.fabric8.kubernetes.api.model.ResourceRequirements;
import io.fabric8.kubernetes.api.model.Toleration;
@@ -23,7 +24,7 @@
import java.util.Map;
import java.util.Set;
-public class GerritSpec {
+public class GerritSpec implements GerritClusterMemberSpec {
private String cluster;
private List<Toleration> tolerations;
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollection.java b/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollection.java
index b25912d..3fc7960 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollection.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollection.java
@@ -14,8 +14,8 @@
package com.google.gerrit.k8s.operator.gitgc;
+import com.google.gerrit.k8s.operator.cluster.GerritClusterMember;
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;
@@ -28,7 +28,7 @@
@ShortNames("gitgc")
@Plural("gitgcs")
public class GitGarbageCollection
- extends CustomResource<GitGarbageCollectionSpec, GitGarbageCollectionStatus>
+ extends GerritClusterMember<GitGarbageCollectionSpec, GitGarbageCollectionStatus>
implements Namespaced {
private static final long serialVersionUID = 1L;
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollectionSpec.java b/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollectionSpec.java
index fbd9458..d0c48f4 100644
--- a/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollectionSpec.java
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/gitgc/GitGarbageCollectionSpec.java
@@ -14,6 +14,7 @@
package com.google.gerrit.k8s.operator.gitgc;
+import com.google.gerrit.k8s.operator.cluster.GerritClusterMemberSpec;
import io.fabric8.kubernetes.api.model.Affinity;
import io.fabric8.kubernetes.api.model.ResourceRequirements;
import io.fabric8.kubernetes.api.model.Toleration;
@@ -22,7 +23,7 @@
import java.util.Objects;
import java.util.Set;
-public class GitGarbageCollectionSpec {
+public class GitGarbageCollectionSpec implements GerritClusterMemberSpec {
private String cluster;
private String schedule;
private Set<String> projects;
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/Receiver.java b/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/Receiver.java
new file mode 100644
index 0000000..758f382
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/Receiver.java
@@ -0,0 +1,35 @@
+// 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.cluster.GerritClusterMember;
+import io.fabric8.kubernetes.api.model.Namespaced;
+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("v1alpha1")
+@ShortNames("grec")
+public class Receiver extends GerritClusterMember<ReceiverSpec, ReceiverStatus>
+ implements Namespaced {
+ private static final long serialVersionUID = 1L;
+
+ public String toString() {
+ return ToStringBuilder.reflectionToString(this, ToStringStyle.JSON_STYLE);
+ }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/ReceiverDeploymentDependentResource.java b/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/ReceiverDeploymentDependentResource.java
new file mode 100644
index 0000000..b50fc5a
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/ReceiverDeploymentDependentResource.java
@@ -0,0 +1,175 @@
+// 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.cluster.GerritCluster;
+import com.google.gerrit.k8s.operator.cluster.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 ReceiverDeploymentDependentResource
+ extends CRUDKubernetesDependentResource<Deployment, Receiver> {
+ public static final int HTTP_PORT = 80;
+
+ public ReceiverDeploymentDependentResource() {
+ super(Deployment.class);
+ }
+
+ @Override
+ protected Deployment desired(Receiver receiver, Context<Receiver> context) {
+ GerritCluster gerritCluster =
+ client
+ .resources(GerritCluster.class)
+ .inNamespace(receiver.getMetadata().getNamespace())
+ .withName(receiver.getSpec().getCluster())
+ .get();
+
+ if (gerritCluster == null) {
+ throw new IllegalStateException("The Gerrit cluster could not be found.");
+ }
+
+ DeploymentBuilder deploymentBuilder = new DeploymentBuilder();
+
+ List<Container> initContainers = new ArrayList<>();
+
+ NfsWorkaroundConfig nfsWorkaround =
+ gerritCluster.getSpec().getStorageClasses().getNfsWorkaround();
+ if (nfsWorkaround.isEnabled() && nfsWorkaround.isChownOnStartup()) {
+ initContainers.add(gerritCluster.createNfsInitContainer());
+ }
+
+ deploymentBuilder
+ .withApiVersion("apps/v1")
+ .withNewMetadata()
+ .withName(receiver.getMetadata().getName())
+ .withNamespace(receiver.getMetadata().getNamespace())
+ .withLabels(getLabels(gerritCluster, receiver))
+ .endMetadata()
+ .withNewSpec()
+ .withReplicas(receiver.getSpec().getReplicas())
+ .withNewStrategy()
+ .withNewRollingUpdate()
+ .withMaxSurge(receiver.getSpec().getMaxSurge())
+ .withMaxUnavailable(receiver.getSpec().getMaxUnavailable())
+ .endRollingUpdate()
+ .endStrategy()
+ .withNewSelector()
+ .withMatchLabels(getLabels(gerritCluster, receiver))
+ .endSelector()
+ .withNewTemplate()
+ .withNewMetadata()
+ .withLabels(getLabels(gerritCluster, receiver))
+ .endMetadata()
+ .withNewSpec()
+ .withTolerations(receiver.getSpec().getTolerations())
+ .withTopologySpreadConstraints(receiver.getSpec().getTopologySpreadConstraints())
+ .withAffinity(receiver.getSpec().getAffinity())
+ .withPriorityClassName(receiver.getSpec().getPriorityClassName())
+ .addAllToImagePullSecrets(gerritCluster.getSpec().getImagePullSecrets())
+ .withNewSecurityContext()
+ .withFsGroup(100L)
+ .endSecurityContext()
+ .addAllToInitContainers(initContainers)
+ .addNewContainer()
+ .withName("apache-git-http-backend")
+ .withImagePullPolicy(gerritCluster.getSpec().getImagePullPolicy())
+ .withImage(
+ gerritCluster.getSpec().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, gerritCluster, false))
+ .endContainer()
+ .addAllToVolumes(getVolumes(receiver, gerritCluster))
+ .endSpec()
+ .endTemplate()
+ .endSpec();
+
+ return deploymentBuilder.build();
+ }
+
+ public static Map<String, String> getLabels(GerritCluster gerritCluster, Receiver receiver) {
+ return gerritCluster.getLabels(
+ String.format("receiver-deployment-%s", receiver.getMetadata().getName()),
+ ReceiverReconciler.class.getSimpleName());
+ }
+
+ private Set<Volume> getVolumes(Receiver receiver, GerritCluster gerritCluster) {
+ Set<Volume> volumes = new HashSet<>();
+ volumes.add(gerritCluster.getGitRepositoriesVolume());
+ volumes.add(gerritCluster.getLogsVolume());
+
+ volumes.add(
+ new VolumeBuilder()
+ .withName(receiver.getSpec().getCredentialSecretRef())
+ .withNewSecret()
+ .withSecretName(receiver.getSpec().getCredentialSecretRef())
+ .endSecret()
+ .build());
+
+ NfsWorkaroundConfig nfsWorkaround =
+ gerritCluster.getSpec().getStorageClasses().getNfsWorkaround();
+ if (nfsWorkaround.isEnabled() && nfsWorkaround.getIdmapdConfig() != null) {
+ volumes.add(gerritCluster.getNfsImapdConfigVolume());
+ }
+
+ return volumes;
+ }
+
+ private Set<VolumeMount> getVolumeMounts(
+ Receiver receiver, GerritCluster gerritCluster, 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 =
+ gerritCluster.getSpec().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/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/ReceiverProbe.java b/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/ReceiverProbe.java
new file mode 100644
index 0000000..8a0c11b
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/ReceiverProbe.java
@@ -0,0 +1,67 @@
+// 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.fasterxml.jackson.annotation.JsonIgnore;
+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;
+
+public class ReceiverProbe extends Probe {
+ private static final long serialVersionUID = 1L;
+
+ private static final HTTPGetAction HTTP_GET_ACTION =
+ new HTTPGetActionBuilder()
+ .withPath("/")
+ .withPort(new IntOrString(ReceiverDeploymentDependentResource.HTTP_PORT))
+ .build();
+
+ @JsonIgnore private ExecAction exec;
+
+ @JsonIgnore private GRPCAction grpc;
+
+ @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 ExecAction getExec() {
+ return null;
+ }
+
+ @Override
+ public GRPCAction getGrpc() {
+ return null;
+ }
+
+ @Override
+ public HTTPGetAction getHttpGet() {
+ return HTTP_GET_ACTION;
+ }
+}
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
new file mode 100644
index 0000000..c1e741b
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/ReceiverReconciler.java
@@ -0,0 +1,159 @@
+// 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.cluster.GerritCluster;
+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.Map;
+import java.util.stream.Collectors;
+
+@ControllerConfiguration(
+ dependents = {
+ @Dependent(name = "receiver-deployment", type = ReceiverDeploymentDependentResource.class),
+ @Dependent(
+ name = "receiver-service",
+ type = ReceiverServiceDependentResource.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;
+
+ public ReceiverReconciler(KubernetesClient client) {
+ this.client = client;
+ }
+
+ @Override
+ public Map<String, EventSource> prepareEventSources(EventSourceContext<Receiver> context) {
+ final SecondaryToPrimaryMapper<GerritCluster> gerritClusterMapper =
+ (GerritCluster cluster) ->
+ context
+ .getPrimaryCache()
+ .list(
+ receiver ->
+ receiver.getSpec().getCluster().equals(cluster.getMetadata().getName()))
+ .map(ResourceID::fromResource)
+ .collect(Collectors.toSet());
+
+ InformerEventSource<GerritCluster, Receiver> gerritClusterEventSource =
+ new InformerEventSource<>(
+ InformerConfiguration.from(GerritCluster.class, context)
+ .withSecondaryToPrimaryMapper(gerritClusterMapper)
+ .build(),
+ 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 =
+ EventSourceInitializer.nameEventSources(gerritClusterEventSource);
+ 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/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/ReceiverServiceConfig.java b/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/ReceiverServiceConfig.java
new file mode 100644
index 0000000..4f4e10a
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/ReceiverServiceConfig.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.receiver;
+
+import java.io.Serializable;
+
+class ReceiverServiceConfig 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/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/ReceiverServiceDependentResource.java b/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/ReceiverServiceDependentResource.java
new file mode 100644
index 0000000..1bce580
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/ReceiverServiceDependentResource.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.receiver;
+
+import static com.google.gerrit.k8s.operator.receiver.ReceiverDeploymentDependentResource.HTTP_PORT;
+
+import com.google.gerrit.k8s.operator.cluster.GerritCluster;
+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 ReceiverServiceDependentResource
+ extends CRUDKubernetesDependentResource<Service, Receiver> {
+ public static final String HTTP_PORT_NAME = "http";
+
+ public ReceiverServiceDependentResource() {
+ super(Service.class);
+ }
+
+ @Override
+ protected Service desired(Receiver receiver, Context<Receiver> context) {
+ GerritCluster gerritCluster =
+ client
+ .resources(GerritCluster.class)
+ .inNamespace(receiver.getMetadata().getNamespace())
+ .withName(receiver.getSpec().getCluster())
+ .get();
+ if (gerritCluster == null) {
+ throw new IllegalStateException("The Gerrit cluster could not be found.");
+ }
+
+ return new ServiceBuilder()
+ .withApiVersion("v1")
+ .withNewMetadata()
+ .withName(getName(receiver))
+ .withNamespace(receiver.getMetadata().getNamespace())
+ .withLabels(getLabels(gerritCluster))
+ .endMetadata()
+ .withNewSpec()
+ .withType(receiver.getSpec().getService().getType())
+ .withPorts(getServicePorts(receiver))
+ .withSelector(ReceiverDeploymentDependentResource.getLabels(gerritCluster, receiver))
+ .endSpec()
+ .build();
+ }
+
+ public static String getName(Receiver receiver) {
+ return receiver.getMetadata().getName();
+ }
+
+ public static Map<String, String> getLabels(GerritCluster gerritCluster) {
+ return gerritCluster.getLabels("receiver-service", ReceiverReconciler.class.getSimpleName());
+ }
+
+ 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/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/ReceiverSpec.java b/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/ReceiverSpec.java
new file mode 100644
index 0000000..5f6245d
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/ReceiverSpec.java
@@ -0,0 +1,151 @@
+// 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.cluster.GerritClusterMemberSpec;
+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 ReceiverSpec implements GerritClusterMemberSpec {
+ private String cluster;
+
+ private List<Toleration> tolerations;
+ 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 ReceiverServiceConfig service = new ReceiverServiceConfig();
+
+ private String credentialSecretRef;
+
+ public String getCluster() {
+ return cluster;
+ }
+
+ public void setCluster(String cluster) {
+ this.cluster = cluster;
+ }
+
+ 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 ReceiverServiceConfig getService() {
+ return service;
+ }
+
+ public void setService(ReceiverServiceConfig service) {
+ this.service = service;
+ }
+
+ public String getCredentialSecretRef() {
+ return credentialSecretRef;
+ }
+
+ public void setCredentialSecretRef(String credentialSecretRef) {
+ this.credentialSecretRef = credentialSecretRef;
+ }
+}
diff --git a/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/ReceiverStatus.java b/operator/src/main/java/com/google/gerrit/k8s/operator/receiver/ReceiverStatus.java
new file mode 100644
index 0000000..60c7ee2
--- /dev/null
+++ b/operator/src/main/java/com/google/gerrit/k8s/operator/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.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/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/GerritE2E.java b/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/GerritE2E.java
index 670368a..c7b904d 100644
--- a/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/GerritE2E.java
+++ b/operator/src/test/java/com/google/gerrit/k8s/operator/gerrit/GerritE2E.java
@@ -14,7 +14,7 @@
package com.google.gerrit.k8s.operator.gerrit;
-import static com.google.gerrit.k8s.operator.test.TestGerritCluster.CLUSTER_NAME;
+import static com.google.gerrit.k8s.operator.cluster.GerritIngress.INGRESS_NAME;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.awaitility.Awaitility.await;
@@ -32,69 +32,23 @@
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.k8s.operator.gerrit.GerritSpec.GerritMode;
import com.google.gerrit.k8s.operator.test.AbstractGerritOperatorE2ETest;
-import com.urswolfer.gerrit.client.rest.GerritAuthData;
-import com.urswolfer.gerrit.client.rest.GerritRestApiFactory;
+import com.google.gerrit.k8s.operator.test.TestGerrit;
import io.fabric8.kubernetes.api.model.LoadBalancerIngress;
-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.api.model.Secret;
-import io.fabric8.kubernetes.api.model.SecretBuilder;
import io.fabric8.kubernetes.api.model.networking.v1.Ingress;
import io.fabric8.kubernetes.api.model.networking.v1.IngressStatus;
-import java.util.Base64;
import java.util.List;
-import java.util.Map;
-import java.util.Set;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
public class GerritE2E extends AbstractGerritOperatorE2ETest {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
- private static final String INGRESS_NAME = "gerrit-ingress";
- private static final String INGRESS_DOMAIN = testProps.getIngressDomain();
- private static final String SECURE_CONFIG_SECRET_NAME = "gerrit-secret";
- private static final String DEFAULT_GERRIT_CONFIG =
- "[gerrit]\n"
- + " serverId = gerrit-1\n"
- + "[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"
- + " listenUrl = proxy-https://*:8080/\n"
- + " requestLog = true\n"
- + " gracefulStopTimeout = 1m\n"
- + "[sshd]\n"
- + " listenAddress = off\n"
- + "[transfer]\n"
- + " timeout = 120 s\n"
- + "[user]\n"
- + " name = Gerrit Code Review\n"
- + " email = gerrit@example.com\n"
- + " anonymousCoward = Unnamed User\n"
- + "[cache]\n"
- + " directory = cache\n"
- + "[container]\n"
- + " javaOptions = -Xmx4g";
-
@Test
void testPrimaryGerritIsCreated() throws Exception {
gerritCluster.setIngressEnabled(true);
- Secret secureConfig = createSecureConfig();
- client.resource(secureConfig).createOrReplace();
-
- Gerrit gerrit = createGerritCR(GerritMode.PRIMARY);
- client.resource(gerrit).createOrReplace();
-
- waitForGerritReadiness(gerrit);
+ TestGerrit testGerrit = new TestGerrit(client, testProps, operator.getNamespace());
+ testGerrit.deploy();
logger.atInfo().log("Waiting max 2 minutes for the Ingress to have an external IP.");
await()
@@ -117,13 +71,7 @@
assertThat(lbIngresses.get(0).getIp(), is(notNullValue()));
});
- GerritApi gerritApi =
- new GerritRestApiFactory()
- .create(
- new GerritAuthData.Basic(
- String.format(
- "http://%s.%s", ServiceDependentResource.getName(gerrit), INGRESS_DOMAIN)));
-
+ GerritApi gerritApi = testGerrit.getGerritApiClient();
await()
.atMost(2, MINUTES)
.untilAsserted(
@@ -137,19 +85,15 @@
@Test
void testGerritReplicaIsCreated() throws Exception {
- Secret secureConfig = createSecureConfig();
- client.resource(secureConfig).createOrReplace();
-
- Gerrit gerrit = createGerritCR(GerritMode.REPLICA);
- client.resource(gerrit).createOrReplace();
-
- waitForGerritReadiness(gerrit);
+ TestGerrit testGerrit =
+ new TestGerrit(client, testProps, operator.getNamespace(), GerritMode.REPLICA);
+ testGerrit.deploy();
assertTrue(
client
.pods()
.inNamespace(operator.getNamespace())
- .withName(gerrit.getMetadata().getName() + "-0")
+ .withName(TestGerrit.NAME + "-0")
.inContainer("gerrit")
.getLog()
.contains("Gerrit Code Review [replica]"));
@@ -157,33 +101,15 @@
@Test
void testRestartHandlingOnConfigChange() {
- Secret secureConfig = createSecureConfig();
- client.resource(secureConfig).createOrReplace();
-
- Gerrit gerrit = createGerritCR(GerritMode.PRIMARY);
- client.resource(gerrit).createOrReplace();
-
- logger.atInfo().log("Waiting max 2 minutes for the Gerrit StatefulSet to be ready.");
- await()
- .atMost(2, MINUTES)
- .untilAsserted(
- () -> {
- assertTrue(
- client
- .apps()
- .statefulSets()
- .inNamespace(operator.getNamespace())
- .withName(gerrit.getMetadata().getName())
- .isReady());
- });
+ TestGerrit testGerrit = new TestGerrit(client, testProps, operator.getNamespace());
+ testGerrit.deploy();
GerritServiceConfig svcConfig = new GerritServiceConfig();
int changedPort = 8081;
svcConfig.setHttpPort(changedPort);
- GerritSpec gerritSpec = gerrit.getSpec();
+ GerritSpec gerritSpec = testGerrit.getSpec();
gerritSpec.setService(svcConfig);
- gerrit.setSpec(gerritSpec);
- client.resource(gerrit).createOrReplace();
+ testGerrit.setSpec(gerritSpec);
await()
.atMost(30, SECONDS)
@@ -193,7 +119,7 @@
client
.services()
.inNamespace(operator.getNamespace())
- .withName(ServiceDependentResource.getName(gerrit))
+ .withName(TestGerrit.NAME)
.get()
.getSpec()
.getPorts()
@@ -202,11 +128,7 @@
});
Mockito.verify(gerritReconciler, times(1)).restartGerritStatefulSet(any());
- String changedConfig =
- DEFAULT_GERRIT_CONFIG.replace("proxy-https://*:8080/", "proxy-https://*:8081/");
- gerritSpec.setConfigFiles(Map.of("gerrit.config", changedConfig));
- gerrit.setSpec(gerritSpec);
- client.resource(gerrit).createOrReplace();
+ testGerrit.modifyGerritConfig("test", "test", "test");
await()
.atMost(2, MINUTES)
@@ -215,11 +137,7 @@
Mockito.verify(gerritReconciler, times(2)).restartGerritStatefulSet(any());
});
- secureConfig.setData(
- Map.of(
- "secure.config",
- Base64.getEncoder().encodeToString("[section]\nkey = value_new".getBytes())));
- client.resource(secureConfig).createOrReplace();
+ testGerrit.modifySecureConfig("test", "test", "test");
await()
.atMost(2, MINUTES)
@@ -228,108 +146,4 @@
Mockito.verify(gerritReconciler, times(3)).restartGerritStatefulSet(any());
});
}
-
- private Gerrit createGerritCR(GerritMode mode) {
- Gerrit gerrit = new Gerrit();
- ObjectMeta gerritMeta =
- new ObjectMetaBuilder().withName("gerrit").withNamespace(operator.getNamespace()).build();
- gerrit.setMetadata(gerritMeta);
- GerritSpec gerritSpec = new GerritSpec();
- gerritSpec.setCluster(CLUSTER_NAME);
- GerritSite site = new GerritSite();
- site.setSize(new Quantity("1Gi"));
- gerritSpec.setMode(mode);
- gerritSpec.setSite(site);
- gerritSpec.setResources(
- new ResourceRequirementsBuilder()
- .withRequests(Map.of("cpu", new Quantity("1"), "memory", new Quantity("5Gi")))
- .build());
- gerritSpec.setConfigFiles(Map.of("gerrit.config", DEFAULT_GERRIT_CONFIG));
- gerritSpec.setSecrets(Set.of(SECURE_CONFIG_SECRET_NAME));
-
- gerrit.setSpec(gerritSpec);
- return gerrit;
- }
-
- private Secret createSecureConfig() {
- return new SecretBuilder()
- .withNewMetadata()
- .withNamespace(operator.getNamespace())
- .withName(SECURE_CONFIG_SECRET_NAME)
- .endMetadata()
- .withData(
- Map.of(
- "secure.config",
- Base64.getEncoder()
- .encodeToString(
- String.format("[ldap]\npassword = %s", testProps.getLdapAdminPwd())
- .getBytes())))
- .build();
- }
-
- private void waitForGerritReadiness(Gerrit gerrit) {
- logger.atInfo().log("Waiting max 1 minutes for the configmaps to be created.");
- await()
- .atMost(1, MINUTES)
- .untilAsserted(
- () -> {
- assertThat(
- client
- .configMaps()
- .inNamespace(operator.getNamespace())
- .withName(GerritConfigMapDependentResource.getName(gerrit))
- .get(),
- is(notNullValue()));
- assertThat(
- client
- .configMaps()
- .inNamespace(operator.getNamespace())
- .withName(GerritInitConfigMapDependentResource.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(operator.getNamespace())
- .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(operator.getNamespace())
- .withName(ServiceDependentResource.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(operator.getNamespace())
- .withName(gerrit.getMetadata().getName())
- .isReady());
- });
- }
}
diff --git a/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/ReceiverE2E.java b/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/ReceiverE2E.java
new file mode 100644
index 0000000..1ddd83b
--- /dev/null
+++ b/operator/src/test/java/com/google/gerrit/k8s/operator/receiver/ReceiverE2E.java
@@ -0,0 +1,223 @@
+// 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 com.google.gerrit.k8s.operator.cluster.GerritIngress.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.equalTo;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.gerrit.k8s.operator.gerrit.GerritSpec.GerritMode;
+import com.google.gerrit.k8s.operator.test.AbstractGerritOperatorE2ETest;
+import com.google.gerrit.k8s.operator.test.TestGerrit;
+import com.google.gerrit.k8s.operator.test.TestGerritCluster;
+import io.fabric8.kubernetes.api.model.LoadBalancerIngress;
+import io.fabric8.kubernetes.api.model.ObjectMeta;
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+import io.fabric8.kubernetes.api.model.Secret;
+import io.fabric8.kubernetes.api.model.SecretBuilder;
+import io.fabric8.kubernetes.api.model.networking.v1.Ingress;
+import io.fabric8.kubernetes.api.model.networking.v1.IngressStatus;
+import java.io.File;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.util.Base64;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.codec.digest.Md5Crypt;
+import org.apache.commons.lang3.RandomStringUtils;
+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.URIish;
+import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestInstance.Lifecycle;
+import org.junit.jupiter.api.io.TempDir;
+
+@TestInstance(Lifecycle.PER_CLASS)
+public class ReceiverE2E extends AbstractGerritOperatorE2ETest {
+ private static final String CREDENTIALS_SECRET_NAME = "receiver-secret";
+ private static final String USER = "git";
+ private static final String PASSWORD = RandomStringUtils.randomAlphanumeric(32);
+
+ private Receiver receiver;
+ private TestGerrit gerritReplica;
+
+ @BeforeAll
+ public void setupReceiver() {
+ receiver = new Receiver();
+ ObjectMeta receiverMeta = new ObjectMetaBuilder().withName("receiver").build();
+ receiver.setMetadata(receiverMeta);
+ ReceiverSpec receiverSpec = new ReceiverSpec();
+ receiverSpec.setCluster(TestGerritCluster.CLUSTER_NAME);
+ receiverSpec.setReplicas(2);
+ receiverSpec.setCredentialSecretRef(CREDENTIALS_SECRET_NAME);
+ receiver.setSpec(receiverSpec);
+ }
+
+ @BeforeEach
+ public void setupComponents() {
+ gerritCluster.setIngressEnabled(true);
+ createCredentialsSecret();
+ client.resource(receiver).inNamespace(operator.getNamespace()).createOrReplace();
+ awaitReceiverReadiness();
+
+ gerritReplica = new TestGerrit(client, testProps, operator.getNamespace(), GerritMode.REPLICA);
+ gerritReplica.deploy();
+ }
+
+ @Test
+ public void testProjectLifecycle(@TempDir Path tempDir) throws Exception {
+ assertThat(sendReceiverApiRequest("GET", "/new/testLegacy.git"), is(equalTo(201)));
+ assertThat(sendReceiverApiRequest("PUT", "/a/projects/test.git"), is(equalTo(201)));
+ CredentialsProvider gerritCredentials =
+ new UsernamePasswordCredentialsProvider(
+ testProps.getGerritUser(), testProps.getGerritPwd());
+ Git git =
+ Git.cloneRepository()
+ .setURI(getGerritUrl("/a/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.remoteAdd()
+ .setName("receiver")
+ .setUri(new URIish(getReceiverUrl("/git/test.git").toString()))
+ .call();
+ git.push()
+ .setCredentialsProvider(new UsernamePasswordCredentialsProvider(USER, PASSWORD))
+ .setRemote("receiver")
+ .setRefSpecs(new RefSpec("refs/heads/master"))
+ .call();
+ assertTrue(
+ git.lsRemote().setCredentialsProvider(gerritCredentials).setRemote("origin").call().stream()
+ .anyMatch(ref -> ref.getObjectId().equals(commit.getId())));
+ assertThat(sendReceiverApiRequest("DELETE", "/a/projects/test.git"), is(equalTo(204)));
+ }
+
+ private void awaitReceiverReadiness() {
+ await()
+ .atMost(1, MINUTES)
+ .untilAsserted(
+ () -> {
+ assertThat(
+ client
+ .services()
+ .inNamespace(operator.getNamespace())
+ .withName(ReceiverServiceDependentResource.getName(receiver))
+ .get(),
+ is(notNullValue()));
+ });
+
+ await()
+ .atMost(2, MINUTES)
+ .untilAsserted(
+ () -> {
+ assertTrue(
+ client
+ .apps()
+ .deployments()
+ .inNamespace(operator.getNamespace())
+ .withName(receiver.getMetadata().getName())
+ .isReady());
+ });
+
+ 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<LoadBalancerIngress> lbIngresses = status.getLoadBalancer().getIngress();
+ assertThat(lbIngresses, hasSize(1));
+ assertThat(lbIngresses.get(0).getIp(), is(notNullValue()));
+ });
+ }
+
+ private int sendReceiverApiRequest(String method, String path) throws Exception {
+ URL url = getReceiverUrl(path);
+
+ HttpURLConnection con = (HttpURLConnection) url.openConnection();
+ try {
+ con.setRequestMethod(method);
+ String encodedAuth =
+ Base64.getEncoder()
+ .encodeToString(
+ String.format("%s:%s", USER, PASSWORD).getBytes(StandardCharsets.UTF_8));
+ con.setRequestProperty("Authorization", "Basic " + encodedAuth);
+ return con.getResponseCode();
+ } finally {
+ con.disconnect();
+ }
+ }
+
+ private URL getReceiverUrl(String path) throws Exception {
+ return new URIBuilder()
+ .setScheme("https")
+ .setHost(
+ String.format("%s.%s", receiver.getMetadata().getName(), testProps.getIngressDomain()))
+ .setPath(path)
+ .build()
+ .toURL();
+ }
+
+ private URL getGerritUrl(String path) throws Exception {
+ return new URIBuilder()
+ .setScheme("https")
+ .setHost(String.format("%s.%s", TestGerrit.NAME, testProps.getIngressDomain()))
+ .setPath(path)
+ .build()
+ .toURL();
+ }
+
+ private void createCredentialsSecret() {
+ String enPasswd = Md5Crypt.md5Crypt(PASSWORD.getBytes());
+ String htpasswdContent = USER + ":" + enPasswd;
+ Secret sec =
+ new SecretBuilder()
+ .withNewMetadata()
+ .withNamespace(operator.getNamespace())
+ .withName(CREDENTIALS_SECRET_NAME)
+ .endMetadata()
+ .withData(
+ Map.of(".htpasswd", Base64.getEncoder().encodeToString(htpasswdContent.getBytes())))
+ .build();
+ client.resource(sec).createOrReplace();
+ }
+}
diff --git a/operator/src/test/java/com/google/gerrit/k8s/operator/test/AbstractGerritOperatorE2ETest.java b/operator/src/test/java/com/google/gerrit/k8s/operator/test/AbstractGerritOperatorE2ETest.java
index c8729a3..6ed616c 100644
--- a/operator/src/test/java/com/google/gerrit/k8s/operator/test/AbstractGerritOperatorE2ETest.java
+++ b/operator/src/test/java/com/google/gerrit/k8s/operator/test/AbstractGerritOperatorE2ETest.java
@@ -21,6 +21,8 @@
import com.google.gerrit.k8s.operator.gerrit.GerritReconciler;
import com.google.gerrit.k8s.operator.gitgc.GitGarbageCollection;
import com.google.gerrit.k8s.operator.gitgc.GitGarbageCollectionReconciler;
+import com.google.gerrit.k8s.operator.receiver.Receiver;
+import com.google.gerrit.k8s.operator.receiver.ReceiverReconciler;
import io.fabric8.kubernetes.api.model.Secret;
import io.fabric8.kubernetes.api.model.SecretBuilder;
import io.fabric8.kubernetes.client.Config;
@@ -53,6 +55,7 @@
.withReconciler(new GerritClusterReconciler(client))
.withReconciler(gerritReconciler)
.withReconciler(new GitGarbageCollectionReconciler(client))
+ .withReconciler(new ReceiverReconciler(client))
.build();
@BeforeEach
@@ -66,6 +69,7 @@
@AfterEach
void cleanup() {
client.resources(Gerrit.class).inNamespace(operator.getNamespace()).delete();
+ client.resources(Receiver.class).inNamespace(operator.getNamespace()).delete();
client.resources(GerritCluster.class).inNamespace(operator.getNamespace()).delete();
client.resources(GitGarbageCollection.class).inNamespace(operator.getNamespace()).delete();
}
diff --git a/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestGerrit.java b/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestGerrit.java
new file mode 100644
index 0000000..5c014f6
--- /dev/null
+++ b/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestGerrit.java
@@ -0,0 +1,255 @@
+// 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.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.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.gerrit.Gerrit;
+import com.google.gerrit.k8s.operator.gerrit.GerritConfigMapDependentResource;
+import com.google.gerrit.k8s.operator.gerrit.GerritInitConfigMapDependentResource;
+import com.google.gerrit.k8s.operator.gerrit.GerritSite;
+import com.google.gerrit.k8s.operator.gerrit.GerritSpec;
+import com.google.gerrit.k8s.operator.gerrit.GerritSpec.GerritMode;
+import com.google.gerrit.k8s.operator.gerrit.ServiceDependentResource;
+import com.urswolfer.gerrit.client.rest.GerritAuthData;
+import com.urswolfer.gerrit.client.rest.GerritRestApiFactory;
+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.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 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 String NAME = "gerrit";
+ private static final String SECURE_CONFIG_SECRET_NAME = "gerrit-secret";
+ private static final String DEFAULT_GERRIT_CONFIG =
+ "[gerrit]\n"
+ + " serverId = gerrit-1\n"
+ + "[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"
+ + " listenUrl = proxy-https://*:8080/\n"
+ + " requestLog = true\n"
+ + " gracefulStopTimeout = 1m\n"
+ + "[sshd]\n"
+ + " listenAddress = off\n"
+ + "[transfer]\n"
+ + " timeout = 120 s\n"
+ + "[user]\n"
+ + " name = Gerrit Code Review\n"
+ + " email = gerrit@example.com\n"
+ + " anonymousCoward = Unnamed User\n"
+ + "[cache]\n"
+ + " directory = cache\n"
+ + "[container]\n"
+ + " javaOptions = -Xmx4g";
+
+ private final KubernetesClient client;
+ private final String namespace;
+ private final GerritMode mode;
+ private final String ingress_domain;
+
+ private Secret secureConfigSecret;
+ private Gerrit gerrit = new Gerrit();
+ private Config config = defaultConfig();
+ private Config secureConfig = new Config();
+
+ public TestGerrit(
+ KubernetesClient client, TestProperties testProps, String namespace, GerritMode mode) {
+ this.client = client;
+ this.namespace = namespace;
+ this.mode = mode;
+ this.ingress_domain = testProps.getIngressDomain();
+ this.secureConfig.setString("ldap", null, "password", testProps.getLdapAdminPwd());
+ }
+
+ public TestGerrit(KubernetesClient client, TestProperties testProps, String namespace) {
+ this(client, testProps, namespace, GerritMode.PRIMARY);
+ }
+
+ public void build() {
+ createGerritCR();
+ createSecureConfig();
+ }
+
+ public void deploy() {
+ build();
+ client.resource(secureConfigSecret).inNamespace(namespace).createOrReplace();
+ client.resource(gerrit).inNamespace(namespace).createOrReplace();
+ waitForGerritReadiness();
+ }
+
+ public GerritApi getGerritApiClient() {
+ return new GerritRestApiFactory()
+ .create(
+ new GerritAuthData.Basic(
+ String.format(
+ "http://%s.%s", ServiceDependentResource.getName(gerrit), ingress_domain)));
+ }
+
+ public void modifyGerritConfig(String section, String key, String value) {
+ config.setString(section, null, key, value);
+ deploy();
+ }
+
+ public void modifySecureConfig(String section, String key, String value) {
+ secureConfig.setString(section, null, key, value);
+ createSecureConfig();
+ client.resource(secureConfigSecret).inNamespace(namespace).createOrReplace();
+ }
+
+ 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;
+ }
+
+ 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.setResources(
+ new ResourceRequirementsBuilder()
+ .withRequests(Map.of("cpu", new Quantity("1"), "memory", new Quantity("5Gi")))
+ .build());
+ }
+ gerritSpec.setCluster(CLUSTER_NAME);
+ gerritSpec.setMode(mode);
+ gerritSpec.setConfigFiles(Map.of("gerrit.config", config.toText()));
+ gerritSpec.setSecrets(Set.of(SECURE_CONFIG_SECRET_NAME));
+
+ gerrit.setSpec(gerritSpec);
+ }
+
+ private void createSecureConfig() {
+ secureConfigSecret =
+ new SecretBuilder()
+ .withNewMetadata()
+ .withNamespace(namespace)
+ .withName(SECURE_CONFIG_SECRET_NAME)
+ .endMetadata()
+ .withData(
+ Map.of(
+ "secure.config",
+ Base64.getEncoder().encodeToString(secureConfig.toText().getBytes())))
+ .build();
+ }
+
+ 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(GerritConfigMapDependentResource.getName(gerrit))
+ .get(),
+ is(notNullValue()));
+ assertThat(
+ client
+ .configMaps()
+ .inNamespace(namespace)
+ .withName(GerritInitConfigMapDependentResource.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(ServiceDependentResource.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/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestProperties.java b/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestProperties.java
index 4581590..e3b58f2 100644
--- a/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestProperties.java
+++ b/operator/src/test/java/com/google/gerrit/k8s/operator/test/TestProperties.java
@@ -63,4 +63,12 @@
public String getLdapAdminPwd() {
return props.getProperty("ldapAdminPwd", "");
}
+
+ public String getGerritUser() {
+ return props.getProperty("gerritUser", "");
+ }
+
+ public String getGerritPwd() {
+ return props.getProperty("gerritPwd", "");
+ }
}
diff --git a/operator/test.properties b/operator/test.properties
index 9e48c77..f8f65f7 100644
--- a/operator/test.properties
+++ b/operator/test.properties
@@ -10,3 +10,8 @@
# Ingress
ingressDomain=
+
+# LDAP
+ldapAdminPwd=
+gerritUser=
+gerritPwd=