Deploy a default ingress along with the Deployment
This commit is contained in:
parent
0de7bae121
commit
c3348c8931
9 changed files with 306 additions and 5 deletions
7
.github/workflows/operator-ci.yml
vendored
7
.github/workflows/operator-ci.yml
vendored
|
@ -41,6 +41,7 @@ jobs:
|
|||
kubernetes version: v1.22.3
|
||||
github token: ${{ secrets.GITHUB_TOKEN }}
|
||||
driver: docker
|
||||
start args: '--addons=ingress'
|
||||
|
||||
- name: Build the Keycloak Docker image
|
||||
run: |
|
||||
|
@ -56,7 +57,8 @@ jobs:
|
|||
mvn clean verify \
|
||||
-Dquarkus.kubernetes.deployment-target=kubernetes \
|
||||
-Doperator.keycloak.image=keycloak:${GITHUB_SHA} \
|
||||
-Doperator.keycloak.image-pull-policy=Never
|
||||
-Doperator.keycloak.image-pull-policy=Never \
|
||||
-Dtest.operator.kubernetes.ip=$(minikube ip)
|
||||
|
||||
- name: Test operator running in cluster
|
||||
working-directory: operator
|
||||
|
@ -66,4 +68,5 @@ jobs:
|
|||
-Dquarkus.container-image.build=true -Dquarkus.container-image.tag=test \
|
||||
-Dquarkus.kubernetes.deployment-target=kubernetes \
|
||||
-Dquarkus.jib.jvm-arguments="-Djava.util.logging.manager=org.jboss.logmanager.LogManager","-Doperator.keycloak.image=keycloak:${GITHUB_SHA}","-Doperator.keycloak.image-pull-policy=Never" \
|
||||
--no-transfer-progress -Dtest.operator.deployment=remote
|
||||
--no-transfer-progress -Dtest.operator.deployment=remote \
|
||||
-Dtest.operator.kubernetes.ip=$(minikube ip)
|
||||
|
|
|
@ -79,4 +79,14 @@ mvn clean verify \
|
|||
-Dquarkus.container-image.tag=test \
|
||||
-Dquarkus.kubernetes.deployment-target=kubernetes \
|
||||
-Dtest.operator.deployment=remote
|
||||
```
|
||||
```
|
||||
|
||||
To run tests on Mac with `minikube` and the `docker` driver you should run `minikube tunnel` in a separate shell and configure the Java properties as follows:
|
||||
```bash
|
||||
-Dtest.operator.kubernetes.ip=localhost
|
||||
```
|
||||
|
||||
On Linux or on Mac using `minikube` on a VM, instead you should pass this additional property:
|
||||
```bash
|
||||
-Dtest.operator.kubernetes.ip=$(minikube ip)
|
||||
```
|
||||
|
|
|
@ -52,6 +52,8 @@ public final class Constants {
|
|||
public static final Integer KEYCLOAK_DISCOVERY_SERVICE_PORT = 7800;
|
||||
public static final String KEYCLOAK_DISCOVERY_SERVICE_SUFFIX = "-discovery";
|
||||
|
||||
public static final String KEYCLOAK_INGRESS_SUFFIX = "-ingress";
|
||||
|
||||
public static final String INSECURE_DISABLE = "INSECURE-DISABLE";
|
||||
public static final String CERTIFICATES_FOLDER = "/mnt/certificates";
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ package org.keycloak.operator.v2alpha1;
|
|||
|
||||
import io.fabric8.kubernetes.api.model.Service;
|
||||
import io.fabric8.kubernetes.api.model.apps.Deployment;
|
||||
import io.fabric8.kubernetes.api.model.networking.v1.Ingress;
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
import io.fabric8.kubernetes.client.informers.SharedIndexInformer;
|
||||
import io.javaoperatorsdk.operator.api.reconciler.Context;
|
||||
|
@ -69,10 +70,16 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
|
|||
.withLabels(Constants.DEFAULT_LABELS)
|
||||
.runnableInformer(0);
|
||||
|
||||
SharedIndexInformer<Ingress> ingressesInformer =
|
||||
client.network().v1().ingresses().inNamespace(context.getConfigurationService().getClientConfiguration().getNamespace())
|
||||
.withLabels(Constants.DEFAULT_LABELS)
|
||||
.runnableInformer(0);
|
||||
|
||||
EventSource deploymentEvent = new InformerEventSource<>(deploymentInformer, Mappers.fromOwnerReference());
|
||||
EventSource servicesEvent = new InformerEventSource<>(servicesInformer, Mappers.fromOwnerReference());
|
||||
EventSource ingressesEvent = new InformerEventSource<>(ingressesInformer, Mappers.fromOwnerReference());
|
||||
|
||||
return List.of(deploymentEvent, servicesEvent);
|
||||
return List.of(deploymentEvent, servicesEvent, ingressesEvent);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -97,6 +104,10 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
|
|||
kcDiscoveryService.updateStatus(statusBuilder);
|
||||
kcDiscoveryService.createOrUpdateReconciled();
|
||||
|
||||
var kcIngress = new KeycloakIngress(client, kc);
|
||||
kcIngress.updateStatus(statusBuilder);
|
||||
kcIngress.createOrUpdateReconciled();
|
||||
|
||||
var status = statusBuilder.build();
|
||||
|
||||
Log.info("--- Reconciliation finished successfully");
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* Copyright 2021 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* 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 org.keycloak.operator.v2alpha1;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.HasMetadata;
|
||||
import io.fabric8.kubernetes.api.model.networking.v1.IngressBuilder;
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
import io.fabric8.kubernetes.api.model.networking.v1.Ingress;
|
||||
import org.keycloak.operator.Constants;
|
||||
import org.keycloak.operator.OperatorManagedResource;
|
||||
import org.keycloak.operator.StatusUpdater;
|
||||
import org.keycloak.operator.v2alpha1.crds.Keycloak;
|
||||
import org.keycloak.operator.v2alpha1.crds.KeycloakStatusBuilder;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Optional;
|
||||
|
||||
public class KeycloakIngress extends OperatorManagedResource implements StatusUpdater<KeycloakStatusBuilder> {
|
||||
|
||||
private Ingress existingIngress;
|
||||
private final Keycloak keycloak;
|
||||
|
||||
public KeycloakIngress(KubernetesClient client, Keycloak keycloakCR) {
|
||||
super(client, keycloakCR);
|
||||
this.keycloak = keycloakCR;
|
||||
this.existingIngress = fetchExistingIngress();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Optional<HasMetadata> getReconciledResource() {
|
||||
var defaultIngress = newIngress();
|
||||
if (keycloak.getSpec().isDefaultIngressDisabled() && existingIngress != null) {
|
||||
client.network().v1().ingresses().delete(existingIngress);
|
||||
return Optional.empty();
|
||||
} else if (existingIngress == null) {
|
||||
return Optional.of(defaultIngress);
|
||||
} else {
|
||||
if (existingIngress.getMetadata().getAnnotations() == null) {
|
||||
existingIngress.getMetadata().setAnnotations(new HashMap<>());
|
||||
}
|
||||
existingIngress.getMetadata().getAnnotations().putAll(defaultIngress.getMetadata().getAnnotations());
|
||||
existingIngress.setSpec(defaultIngress.getSpec());
|
||||
return Optional.of(existingIngress);
|
||||
}
|
||||
}
|
||||
|
||||
private Ingress newIngress() {
|
||||
var port = (keycloak.getSpec().isHttp()) ? Constants.KEYCLOAK_HTTP_PORT : Constants.KEYCLOAK_HTTPS_PORT;
|
||||
var backendProtocol = (keycloak.getSpec().isHttp()) ? "HTTP" : "HTTPS";
|
||||
|
||||
Ingress ingress = new IngressBuilder()
|
||||
.withNewMetadata()
|
||||
.withName(getName())
|
||||
.withNamespace(getNamespace())
|
||||
.addToAnnotations("nginx.ingress.kubernetes.io/backend-protocol", backendProtocol)
|
||||
.endMetadata()
|
||||
.withNewSpec()
|
||||
.withNewDefaultBackend()
|
||||
.withNewService()
|
||||
.withName(keycloak.getMetadata().getName() + Constants.KEYCLOAK_SERVICE_SUFFIX)
|
||||
.withNewPort()
|
||||
.withNumber(port)
|
||||
.endPort()
|
||||
.endService()
|
||||
.endDefaultBackend()
|
||||
.addNewRule()
|
||||
.withNewHttp()
|
||||
.addNewPath()
|
||||
.withPath("/")
|
||||
.withPathType("ImplementationSpecific")
|
||||
.withNewBackend()
|
||||
.withNewService()
|
||||
.withName(keycloak.getMetadata().getName() + Constants.KEYCLOAK_SERVICE_SUFFIX)
|
||||
.withNewPort()
|
||||
.withNumber(port)
|
||||
.endPort()
|
||||
.endService()
|
||||
.endBackend()
|
||||
.endPath()
|
||||
.endHttp()
|
||||
.endRule()
|
||||
.endSpec()
|
||||
.build();
|
||||
|
||||
if (!keycloak.getSpec().isHostnameDisabled()) {
|
||||
ingress.getSpec().getRules().get(0).setHost(keycloak.getSpec().getHostname());
|
||||
}
|
||||
|
||||
return ingress;
|
||||
}
|
||||
|
||||
private Ingress fetchExistingIngress() {
|
||||
return client
|
||||
.network()
|
||||
.v1()
|
||||
.ingresses()
|
||||
.inNamespace(getNamespace())
|
||||
.withName(getName())
|
||||
.get();
|
||||
}
|
||||
|
||||
public void updateStatus(KeycloakStatusBuilder status) {
|
||||
if (existingIngress == null) {
|
||||
status.addNotReadyMessage("No existing Keycloak Ingress found, waiting for creating a new one");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return cr.getMetadata().getName() + Constants.KEYCLOAK_INGRESS_SUFFIX;
|
||||
}
|
||||
}
|
|
@ -39,7 +39,8 @@ public class KeycloakSpec {
|
|||
@JsonPropertyDescription("A secret containing the TLS configuration for HTTPS. Reference: https://kubernetes.io/docs/concepts/configuration/secret/#tls-secrets.\n" +
|
||||
"The special value `" + Constants.INSECURE_DISABLE + "` disables https.")
|
||||
private String tlsSecret;
|
||||
|
||||
@JsonPropertyDescription("Disable the default ingress.")
|
||||
private boolean disableDefaultIngress;
|
||||
@JsonPropertyDescription("List of URLs to download Keycloak extensions.")
|
||||
private List<String> extensions;
|
||||
@JsonPropertyDescription(
|
||||
|
@ -59,6 +60,14 @@ public class KeycloakSpec {
|
|||
return this.hostname.equals(Constants.INSECURE_DISABLE);
|
||||
}
|
||||
|
||||
public void setDefaultIngressDisabled(boolean value) {
|
||||
this.disableDefaultIngress = value;
|
||||
}
|
||||
|
||||
public boolean isDefaultIngressDisabled() {
|
||||
return this.disableDefaultIngress;
|
||||
}
|
||||
|
||||
public String getTlsSecret() {
|
||||
return tlsSecret;
|
||||
}
|
||||
|
|
|
@ -40,6 +40,18 @@ rules:
|
|||
- delete
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- networking.k8s.io
|
||||
resources:
|
||||
- ingresses
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- create
|
||||
- delete
|
||||
- patch
|
||||
- update
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
|
|
|
@ -39,6 +39,7 @@ public abstract class ClusterOperatorTest {
|
|||
public static final String QUARKUS_KUBERNETES_DEPLOYMENT_TARGET = "quarkus.kubernetes.deployment-target";
|
||||
public static final String OPERATOR_DEPLOYMENT_PROP = "test.operator.deployment";
|
||||
public static final String TARGET_KUBERNETES_GENERATED_YML_FOLDER = "target/kubernetes/";
|
||||
public static final String OPERATOR_KUBERNETES_IP = "test.operator.kubernetes.ip";
|
||||
|
||||
public static final String TEST_RESULTS_DIR = "target/operator-test-results/";
|
||||
public static final String POD_LOGS_DIR = TEST_RESULTS_DIR + "pod-logs/";
|
||||
|
@ -51,6 +52,7 @@ public abstract class ClusterOperatorTest {
|
|||
protected static KubernetesClient k8sclient;
|
||||
protected static String namespace;
|
||||
protected static String deploymentTarget;
|
||||
protected static String kubernetesIp;
|
||||
private static Operator operator;
|
||||
|
||||
|
||||
|
@ -60,6 +62,7 @@ public abstract class ClusterOperatorTest {
|
|||
reconcilers = CDI.current().select(new TypeLiteral<>() {});
|
||||
operatorDeployment = ConfigProvider.getConfig().getOptionalValue(OPERATOR_DEPLOYMENT_PROP, OperatorDeployment.class).orElse(OperatorDeployment.local);
|
||||
deploymentTarget = ConfigProvider.getConfig().getOptionalValue(QUARKUS_KUBERNETES_DEPLOYMENT_TARGET, String.class).orElse("kubernetes");
|
||||
kubernetesIp = ConfigProvider.getConfig().getOptionalValue(OPERATOR_KUBERNETES_IP, String.class).orElse("localhost");
|
||||
|
||||
setDefaultAwaitilityTimings();
|
||||
calculateNamespace();
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
package org.keycloak.operator;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.networking.v1.ServiceBackendPortBuilder;
|
||||
import io.quarkus.logging.Log;
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import io.restassured.RestAssured;
|
||||
import org.awaitility.Awaitility;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.operator.utils.K8sUtils;
|
||||
import org.keycloak.operator.v2alpha1.KeycloakIngress;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
@QuarkusTest
|
||||
public class KeycloakIngressE2EIT extends ClusterOperatorTest {
|
||||
|
||||
@Test
|
||||
public void testIngressOnHTTP() {
|
||||
var kc = K8sUtils.getDefaultKeycloakDeployment();
|
||||
kc.getSpec().setHostname(Constants.INSECURE_DISABLE);
|
||||
kc.getSpec().setTlsSecret(Constants.INSECURE_DISABLE);
|
||||
K8sUtils.deployKeycloak(k8sclient, kc, true);
|
||||
|
||||
Awaitility.await()
|
||||
.ignoreExceptions()
|
||||
.untilAsserted(() -> {
|
||||
var output = RestAssured.given()
|
||||
.get("http://" + kubernetesIp + ":80/realms/master")
|
||||
.body()
|
||||
.jsonPath()
|
||||
.getString("realm");
|
||||
|
||||
assertEquals("master", output);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIngressOnHTTPS() {
|
||||
var kc = K8sUtils.getDefaultKeycloakDeployment();
|
||||
kc.getSpec().setHostname(Constants.INSECURE_DISABLE);
|
||||
K8sUtils.deployKeycloak(k8sclient, kc, true);
|
||||
|
||||
Awaitility.await()
|
||||
.ignoreExceptions()
|
||||
.untilAsserted(() -> {
|
||||
var output = RestAssured.given()
|
||||
.relaxedHTTPSValidation()
|
||||
.get("https://" + kubernetesIp + ":443/realms/master")
|
||||
.body()
|
||||
.jsonPath()
|
||||
.getString("realm");
|
||||
|
||||
assertEquals("master", output);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIngressHostname() {
|
||||
var kc = K8sUtils.getDefaultKeycloakDeployment();
|
||||
kc.getSpec().setHostname("foo.bar");
|
||||
K8sUtils.deployKeycloak(k8sclient, kc, true);
|
||||
|
||||
var ingress = new KeycloakIngress(k8sclient, kc);
|
||||
Awaitility.await()
|
||||
.ignoreExceptions()
|
||||
.untilAsserted(() -> {
|
||||
var host = k8sclient
|
||||
.network()
|
||||
.v1()
|
||||
.ingresses()
|
||||
.inNamespace(namespace)
|
||||
.withName(ingress.getName())
|
||||
.get()
|
||||
.getSpec()
|
||||
.getRules()
|
||||
.get(0)
|
||||
.getHost();
|
||||
|
||||
assertEquals("foo.bar", host);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMainIngressDurability() {
|
||||
var kc = K8sUtils.getDefaultKeycloakDeployment();
|
||||
K8sUtils.deployKeycloak(k8sclient, kc, true);
|
||||
|
||||
var ingress = new KeycloakIngress(k8sclient, kc);
|
||||
var ingressSelector = k8sclient
|
||||
.network()
|
||||
.v1()
|
||||
.ingresses()
|
||||
.inNamespace(namespace)
|
||||
.withName(ingress.getName());
|
||||
|
||||
Log.info("Trying to delete the ingress");
|
||||
assertThat(ingressSelector.delete()).isTrue();
|
||||
Awaitility.await()
|
||||
.untilAsserted(() -> assertThat(ingressSelector.get()).isNotNull());
|
||||
|
||||
K8sUtils.waitForKeycloakToBeReady(k8sclient, kc); // wait for reconciler to calm down to avoid race condititon
|
||||
|
||||
Log.info("Trying to modify the ingress");
|
||||
|
||||
var currentIngress = ingressSelector.get();
|
||||
var labels = Map.of("address", "EvergreenTerrace742");
|
||||
currentIngress.getSpec().getDefaultBackend().getService().setPort(new ServiceBackendPortBuilder().withName("foo").build());
|
||||
|
||||
currentIngress.getMetadata().getAnnotations().clear();
|
||||
currentIngress.getMetadata().getLabels().putAll(labels);
|
||||
|
||||
ingressSelector.createOrReplace(currentIngress);
|
||||
|
||||
Awaitility.await()
|
||||
.untilAsserted(() -> {
|
||||
var i = ingressSelector.get();
|
||||
assertThat(i.getMetadata().getLabels().entrySet().containsAll(labels.entrySet())).isTrue(); // additional labels should not be overwritten
|
||||
assertEquals("HTTPS", i.getMetadata().getAnnotations().get("nginx.ingress.kubernetes.io/backend-protocol"));
|
||||
assertEquals(Constants.KEYCLOAK_HTTPS_PORT, i.getSpec().getDefaultBackend().getService().getPort().getNumber());
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue