converts the ingress logic to a conditional dependent resource (#22221)

Closes #22206
This commit is contained in:
Steven Hawkins 2023-08-21 13:34:59 -04:00 committed by GitHub
parent 0fda336ac1
commit 6b0e1f87f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 63 additions and 121 deletions

View file

@ -18,7 +18,6 @@ package org.keycloak.operator.controllers;
import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.Service;
import io.fabric8.kubernetes.api.model.apps.StatefulSet; import io.fabric8.kubernetes.api.model.apps.StatefulSet;
import io.fabric8.kubernetes.api.model.networking.v1.Ingress;
import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient;
import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.Context;
@ -52,6 +51,7 @@ import static io.javaoperatorsdk.operator.api.reconciler.Constants.WATCH_CURRENT
@ControllerConfiguration(namespaces = WATCH_CURRENT_NAMESPACE, @ControllerConfiguration(namespaces = WATCH_CURRENT_NAMESPACE,
dependents = { dependents = {
@Dependent(type = KeycloakAdminSecretDependentResource.class), @Dependent(type = KeycloakAdminSecretDependentResource.class),
@Dependent(type = KeycloakIngressDependentResource.class, reconcilePrecondition = KeycloakIngressDependentResource.EnabledCondition.class),
@Dependent(type = KeycloakServiceDependentResource.class, useEventSourceWithName = "serviceSource"), @Dependent(type = KeycloakServiceDependentResource.class, useEventSourceWithName = "serviceSource"),
@Dependent(type = KeycloakDiscoveryServiceDependentResource.class, useEventSourceWithName = "serviceSource") @Dependent(type = KeycloakDiscoveryServiceDependentResource.class, useEventSourceWithName = "serviceSource")
}) })
@ -86,22 +86,13 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
.withOnUpdateFilter(new MetadataAwareOnUpdateFilter<>()) .withOnUpdateFilter(new MetadataAwareOnUpdateFilter<>())
.build(); .build();
InformerConfiguration<Ingress> ingressesIC = InformerConfiguration
.from(Ingress.class)
.withLabelSelector(Constants.DEFAULT_LABELS_AS_STRING)
.withNamespaces(namespace)
.withSecondaryToPrimaryMapper(Mappers.fromOwnerReference())
.withOnUpdateFilter(new MetadataAwareOnUpdateFilter<>())
.build();
EventSource statefulSetEvent = new InformerEventSource<>(statefulSetIC, context); EventSource statefulSetEvent = new InformerEventSource<>(statefulSetIC, context);
EventSource servicesEvent = new InformerEventSource<>(servicesIC, context); EventSource servicesEvent = new InformerEventSource<>(servicesIC, context);
EventSource ingressesEvent = new InformerEventSource<>(ingressesIC, context);
Map<String, EventSource> sources = new HashMap<>(); Map<String, EventSource> sources = new HashMap<>();
sources.put("serviceSource", servicesEvent); sources.put("serviceSource", servicesEvent);
sources.putAll(EventSourceInitializer.nameEventSources(statefulSetEvent, sources.putAll(EventSourceInitializer.nameEventSources(statefulSetEvent,
ingressesEvent, watchedSecrets.getWatchedSecretsEventSource())); watchedSecrets.getWatchedSecretsEventSource()));
return sources; return sources;
} }
@ -126,9 +117,6 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
kcDeployment.createOrUpdateReconciled(); kcDeployment.createOrUpdateReconciled();
kcDeployment.updateStatus(statusAggregator); kcDeployment.updateStatus(statusAggregator);
var kcIngress = new KeycloakIngress(client, kc);
kcIngress.createOrUpdateReconciled();
var status = statusAggregator.build(); var status = statusAggregator.build();
Log.info("--- Reconciliation finished successfully"); Log.info("--- Reconciliation finished successfully");

View file

@ -16,10 +16,13 @@
*/ */
package org.keycloak.operator.controllers; package org.keycloak.operator.controllers;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.networking.v1.Ingress; import io.fabric8.kubernetes.api.model.networking.v1.Ingress;
import io.fabric8.kubernetes.api.model.networking.v1.IngressBuilder; import io.fabric8.kubernetes.api.model.networking.v1.IngressBuilder;
import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
import org.keycloak.operator.Constants; import org.keycloak.operator.Constants;
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
@ -30,32 +33,27 @@ import java.util.Optional;
import static org.keycloak.operator.crds.v2alpha1.CRDUtils.isTlsConfigured; import static org.keycloak.operator.crds.v2alpha1.CRDUtils.isTlsConfigured;
public class KeycloakIngress extends OperatorManagedResource { @KubernetesDependent(labelSelector = Constants.DEFAULT_LABELS_AS_STRING)
public class KeycloakIngressDependentResource extends CRUDKubernetesDependentResource<Ingress, Keycloak> {
private final Ingress existingIngress; public static class EnabledCondition implements Condition<Ingress, Keycloak> {
private final Keycloak keycloak; @Override
public boolean isMet(DependentResource<Ingress, Keycloak> dependentResource, Keycloak primary,
public KeycloakIngress(KubernetesClient client, Keycloak keycloakCR) { Context<Keycloak> context) {
super(client, keycloakCR); return isIngressEnabled(primary);
this.keycloak = keycloakCR;
this.existingIngress = fetchExistingIngress();
}
@Override
protected Optional<HasMetadata> getReconciledResource() {
IngressSpec ingressSpec = keycloak.getSpec().getIngressSpec();
if (ingressSpec != null && !ingressSpec.isIngressEnabled()) {
if (existingIngress != null && existingIngress.hasOwnerReferenceFor(keycloak)) {
deleteExistingIngress();
}
return Optional.empty();
} else {
return Optional.of(newIngress());
} }
} }
private Ingress newIngress() { public KeycloakIngressDependentResource() {
// set default annotations super(Ingress.class);
}
public static boolean isIngressEnabled(Keycloak keycloak) {
return Optional.ofNullable(keycloak.getSpec().getIngressSpec()).map(IngressSpec::isIngressEnabled).orElse(true);
}
@Override
public Ingress desired(Keycloak keycloak, Context<Keycloak> context) {
var annotations = new HashMap<String, String>(); var annotations = new HashMap<String, String>();
boolean tlsConfigured = isTlsConfigured(keycloak); boolean tlsConfigured = isTlsConfigured(keycloak);
var port = KeycloakServiceDependentResource.getServicePort(tlsConfigured, keycloak); var port = KeycloakServiceDependentResource.getServicePort(tlsConfigured, keycloak);
@ -73,8 +71,10 @@ public class KeycloakIngress extends OperatorManagedResource {
Ingress ingress = new IngressBuilder() Ingress ingress = new IngressBuilder()
.withNewMetadata() .withNewMetadata()
.withName(getName()) .withName(getName(keycloak))
.withNamespace(getNamespace()) .withNamespace(keycloak.getMetadata().getNamespace())
.addToLabels(Constants.DEFAULT_LABELS)
.addToLabels(OperatorManagedResource.updateWithInstanceLabels(null, keycloak.getMetadata().getName()))
.addToAnnotations(annotations) .addToAnnotations(annotations)
.endMetadata() .endMetadata()
.withNewSpec() .withNewSpec()
@ -84,21 +84,18 @@ public class KeycloakIngress extends OperatorManagedResource {
.withName(KeycloakServiceDependentResource.getServiceName(keycloak)) .withName(KeycloakServiceDependentResource.getServiceName(keycloak))
.withNewPort() .withNewPort()
.withNumber(port) .withNumber(port)
.withName("") // for SSA to clear the name if already set
.endPort() .endPort()
.endService() .endService()
.endDefaultBackend() .endDefaultBackend()
.addNewRule() .addNewRule()
.withNewHttp() .withNewHttp()
.addNewPath() .addNewPath()
.withPath("")
.withPathType("ImplementationSpecific") .withPathType("ImplementationSpecific")
.withNewBackend() .withNewBackend()
.withNewService() .withNewService()
.withName(KeycloakServiceDependentResource.getServiceName(keycloak)) .withName(KeycloakServiceDependentResource.getServiceName(keycloak))
.withNewPort() .withNewPort()
.withNumber(port) .withNumber(port)
.withName("") // for SSA to clear the name if already set
.endPort() .endPort()
.endService() .endService()
.endBackend() .endBackend()
@ -116,22 +113,7 @@ public class KeycloakIngress extends OperatorManagedResource {
return ingress; return ingress;
} }
protected void deleteExistingIngress() { public static String getName(Keycloak keycloak) {
client.resource(existingIngress).delete(); return keycloak.getMetadata().getName() + Constants.KEYCLOAK_INGRESS_SUFFIX;
}
protected Ingress fetchExistingIngress() {
return client
.network()
.v1()
.ingresses()
.inNamespace(getNamespace())
.withName(getName())
.get();
}
@Override
public String getName() {
return cr.getMetadata().getName() + Constants.KEYCLOAK_INGRESS_SUFFIX;
} }
} }

View file

@ -24,17 +24,18 @@ import io.fabric8.kubernetes.client.dsl.Resource;
import io.quarkus.logging.Log; import io.quarkus.logging.Log;
import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured; import io.restassured.RestAssured;
import org.awaitility.Awaitility; import org.awaitility.Awaitility;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.keycloak.operator.Constants; import org.keycloak.operator.Constants;
import org.keycloak.operator.controllers.KeycloakIngressDependentResource;
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.HostnameSpecBuilder;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.IngressSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.IngressSpec;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.IngressSpecBuilder; import org.keycloak.operator.crds.v2alpha1.deployment.spec.IngressSpecBuilder;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.HostnameSpecBuilder;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.UnsupportedSpecBuilder; import org.keycloak.operator.crds.v2alpha1.deployment.spec.UnsupportedSpecBuilder;
import org.keycloak.operator.testsuite.utils.K8sUtils; import org.keycloak.operator.testsuite.utils.K8sUtils;
import org.keycloak.operator.controllers.KeycloakIngress;
import java.util.Map; import java.util.Map;
@ -162,7 +163,6 @@ public class KeycloakIngressTest extends BaseOperatorTest {
K8sUtils.deployKeycloak(k8sclient, kc, true); K8sUtils.deployKeycloak(k8sclient, kc, true);
var ingress = new KeycloakIngress(k8sclient, kc);
Awaitility.await() Awaitility.await()
.ignoreExceptions() .ignoreExceptions()
.untilAsserted(() -> { .untilAsserted(() -> {
@ -171,7 +171,7 @@ public class KeycloakIngressTest extends BaseOperatorTest {
.v1() .v1()
.ingresses() .ingresses()
.inNamespace(namespace) .inNamespace(namespace)
.withName(ingress.getName()) .withName(KeycloakIngressDependentResource.getName(kc))
.get() .get()
.getSpec() .getSpec()
.getRules() .getRules()
@ -190,13 +190,12 @@ public class KeycloakIngressTest extends BaseOperatorTest {
kc.getSpec().getIngressSpec().setAnnotations(Map.of("haproxy.router.openshift.io/disable_cookies", "true")); kc.getSpec().getIngressSpec().setAnnotations(Map.of("haproxy.router.openshift.io/disable_cookies", "true"));
K8sUtils.deployKeycloak(k8sclient, kc, true); K8sUtils.deployKeycloak(k8sclient, kc, true);
var ingress = new KeycloakIngress(k8sclient, kc);
var ingressSelector = k8sclient var ingressSelector = k8sclient
.network() .network()
.v1() .v1()
.ingresses() .ingresses()
.inNamespace(namespace) .inNamespace(namespace)
.withName(ingress.getName()); .withName(KeycloakIngressDependentResource.getName(kc));
Log.info("Trying to delete the ingress"); Log.info("Trying to delete the ingress");
assertThat(ingressSelector.delete()).isNotNull(); assertThat(ingressSelector.delete()).isNotNull();
@ -210,7 +209,7 @@ public class KeycloakIngressTest extends BaseOperatorTest {
var labels = Map.of("address", "EvergreenTerrace742"); var labels = Map.of("address", "EvergreenTerrace742");
ingressSelector.accept(currentIngress -> { ingressSelector.accept(currentIngress -> {
currentIngress.getMetadata().setResourceVersion(null); currentIngress.getMetadata().setResourceVersion(null);
currentIngress.getSpec().getDefaultBackend().getService().setPort(new ServiceBackendPortBuilder().withName("foo").build()); currentIngress.getSpec().getDefaultBackend().getService().setPort(new ServiceBackendPortBuilder().withNumber(6500).build());
currentIngress.getMetadata().getAnnotations().clear(); currentIngress.getMetadata().getAnnotations().clear();
currentIngress.getMetadata().getLabels().putAll(labels); currentIngress.getMetadata().getLabels().putAll(labels);
@ -260,7 +259,7 @@ public class KeycloakIngressTest extends BaseOperatorTest {
assertThat(k8sclient.network().v1().ingresses().inNamespace(namespace).list().getItems().size()).isEqualTo(1); assertThat(k8sclient.network().v1().ingresses().inNamespace(namespace).list().getItems().size()).isEqualTo(1);
}); });
Log.info("Redeploying the Keycloak CR with default Ingress disabled"); Log.info("Deploying the Keycloak CR with default Ingress disabled");
defaultKeycloakDeployment.getSpec().setIngressSpec(new IngressSpec()); defaultKeycloakDeployment.getSpec().setIngressSpec(new IngressSpec());
defaultKeycloakDeployment.getSpec().getIngressSpec().setIngressEnabled(false); defaultKeycloakDeployment.getSpec().getIngressSpec().setIngressEnabled(false);
@ -288,13 +287,12 @@ public class KeycloakIngressTest extends BaseOperatorTest {
kc.getSpec().setIngressSpec(new IngressSpecBuilder().withIngressClassName("nginx").build()); kc.getSpec().setIngressSpec(new IngressSpecBuilder().withIngressClassName("nginx").build());
K8sUtils.deployKeycloak(k8sclient, kc, true); K8sUtils.deployKeycloak(k8sclient, kc, true);
var ingress = new KeycloakIngress(k8sclient, kc);
var ingressSelector = k8sclient var ingressSelector = k8sclient
.network() .network()
.v1() .v1()
.ingresses() .ingresses()
.inNamespace(namespace) .inNamespace(namespace)
.withName(ingress.getName()); .withName(KeycloakIngressDependentResource.getName(kc));
Awaitility.await() Awaitility.await()
.ignoreExceptions() .ignoreExceptions()
@ -325,13 +323,12 @@ public class KeycloakIngressTest extends BaseOperatorTest {
kc.getSpec().getIngressSpec().setAnnotations(Map.of("a", "b")); kc.getSpec().getIngressSpec().setAnnotations(Map.of("a", "b"));
K8sUtils.deployKeycloak(k8sclient, kc, true); K8sUtils.deployKeycloak(k8sclient, kc, true);
var ingress = new KeycloakIngress(k8sclient, kc);
var ingressSelector = k8sclient var ingressSelector = k8sclient
.network() .network()
.v1() .v1()
.ingresses() .ingresses()
.inNamespace(namespace) .inNamespace(namespace)
.withName(ingress.getName()); .withName(KeycloakIngressDependentResource.getName(kc));
Awaitility.await() Awaitility.await()
.ignoreExceptions() .ignoreExceptions()

View file

@ -17,22 +17,18 @@
package org.keycloak.operator.testsuite.unit; package org.keycloak.operator.testsuite.unit;
import java.util.Collections; import io.fabric8.kubernetes.api.model.HasMetadata;
import java.util.Map; import io.fabric8.kubernetes.api.model.networking.v1.Ingress;
import java.util.Optional;
import io.fabric8.kubernetes.api.model.OwnerReference;
import io.fabric8.kubernetes.api.model.OwnerReferenceBuilder;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.keycloak.operator.controllers.KeycloakIngress; import org.keycloak.operator.controllers.KeycloakIngressDependentResource;
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.IngressSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.IngressSpec;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.IngressSpecBuilder; import org.keycloak.operator.crds.v2alpha1.deployment.spec.IngressSpecBuilder;
import org.keycloak.operator.testsuite.utils.K8sUtils; import org.keycloak.operator.testsuite.utils.K8sUtils;
import io.fabric8.kubernetes.api.model.HasMetadata; import java.util.Map;
import io.fabric8.kubernetes.api.model.networking.v1.Ingress; import java.util.Optional;
import io.fabric8.kubernetes.api.model.networking.v1.IngressBuilder;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
@ -43,7 +39,7 @@ public class IngressLogicTest {
private static final String EXISTING_ANNOTATION_KEY = "annotation"; private static final String EXISTING_ANNOTATION_KEY = "annotation";
static class MockKeycloakIngress extends KeycloakIngress { static class MockKeycloakIngress {
private static Keycloak getKeycloak(boolean tlsConfigured, IngressSpec ingressSpec) { private static Keycloak getKeycloak(boolean tlsConfigured, IngressSpec ingressSpec) {
var kc = K8sUtils.getDefaultKeycloakDeployment(); var kc = K8sUtils.getDefaultKeycloakDeployment();
@ -66,7 +62,6 @@ public class IngressLogicTest {
} }
public static MockKeycloakIngress build(Boolean defaultIngressEnabled, boolean ingressExists, boolean ingressSpecDefined, boolean tlsConfigured, Map<String, String> annotations) { public static MockKeycloakIngress build(Boolean defaultIngressEnabled, boolean ingressExists, boolean ingressSpecDefined, boolean tlsConfigured, Map<String, String> annotations) {
MockKeycloakIngress.ingressExists = ingressExists;
IngressSpec ingressSpec = null; IngressSpec ingressSpec = null;
if (ingressSpecDefined) { if (ingressSpecDefined) {
ingressSpec = new IngressSpec(); ingressSpec = new IngressSpec();
@ -77,18 +72,18 @@ public class IngressLogicTest {
ingressSpec.setAnnotations(annotations); ingressSpec.setAnnotations(annotations);
} }
} }
return new MockKeycloakIngress(tlsConfigured, ingressSpec); MockKeycloakIngress mock = new MockKeycloakIngress(tlsConfigured, ingressSpec);
mock.ingressExists = ingressExists;
return mock;
} }
public static boolean ingressExists = false; private KeycloakIngressDependentResource keycloakIngressDependentResource = new KeycloakIngressDependentResource();
private boolean ingressExists = false;
private boolean deleted = false; private boolean deleted = false;
public MockKeycloakIngress(boolean tlsConfigured, IngressSpec ingressSpec) { private Keycloak keycloak;
super(null, getKeycloak(tlsConfigured, ingressSpec));
}
@Override public MockKeycloakIngress(boolean tlsConfigured, IngressSpec ingressSpec) {
public Optional<HasMetadata> getReconciledResource() { this.keycloak = getKeycloak(tlsConfigured, ingressSpec);
return super.getReconciledResource();
} }
public boolean reconciled() { public boolean reconciled() {
@ -99,35 +94,15 @@ public class IngressLogicTest {
return deleted; return deleted;
} }
@Override public Optional<HasMetadata> getReconciledResource() {
protected Ingress fetchExistingIngress() { if (!KeycloakIngressDependentResource.isIngressEnabled(keycloak)) {
if (ingressExists) { if (ingressExists) {
deleted = true;
OwnerReference sameCROwnerRef = new OwnerReferenceBuilder() }
.withApiVersion(cr.getApiVersion()) return Optional.empty();
.withKind(cr.getKind())
.withName(cr.getMetadata().getName())
.withUid(cr.getMetadata().getUid())
.withBlockOwnerDeletion(true)
.withController(true)
.build();
return new IngressBuilder()
.withNewMetadata()
.withName(getName())
.withNamespace(cr.getMetadata().getNamespace())
.withOwnerReferences(Collections.singletonList(sameCROwnerRef))
.withAnnotations(Map.of(EXISTING_ANNOTATION_KEY, "value"))
.endMetadata()
.build();
} else {
return null;
} }
}
return Optional.of(keycloakIngressDependentResource.desired(keycloak, null));
@Override
protected void deleteExistingIngress() {
deleted = true;
} }
} }
@ -226,7 +201,7 @@ public class IngressLogicTest {
assertEquals("passthrough", reconciled.get().getMetadata().getAnnotations().get("route.openshift.io/termination")); assertEquals("passthrough", reconciled.get().getMetadata().getAnnotations().get("route.openshift.io/termination"));
assertEquals("another-value", reconciled.get().getMetadata().getAnnotations().get(EXISTING_ANNOTATION_KEY)); assertEquals("another-value", reconciled.get().getMetadata().getAnnotations().get(EXISTING_ANNOTATION_KEY));
} }
@Test @Test
public void testIngressSpecDefinedWithoutClassName() { public void testIngressSpecDefinedWithoutClassName() {
var kc = new MockKeycloakIngress(true, new IngressSpec()); var kc = new MockKeycloakIngress(true, new IngressSpec());
@ -234,7 +209,7 @@ public class IngressLogicTest {
Ingress ingress = reconciled.map(Ingress.class::cast).orElseThrow(); Ingress ingress = reconciled.map(Ingress.class::cast).orElseThrow();
assertNull(ingress.getSpec().getIngressClassName()); assertNull(ingress.getSpec().getIngressClassName());
} }
@Test @Test
public void testIngressSpecDefinedWithClassName() { public void testIngressSpecDefinedWithClassName() {
var kc = new MockKeycloakIngress(true, new IngressSpecBuilder().withIngressClassName("my-class").build()); var kc = new MockKeycloakIngress(true, new IngressSpecBuilder().withIngressClassName("my-class").build());