diff --git a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakIngress.java b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakIngress.java index c2c62b7cb8..63700572c9 100644 --- a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakIngress.java +++ b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakIngress.java @@ -53,10 +53,7 @@ public class KeycloakIngress extends OperatorManagedResource implements StatusUp var defaultIngress = newIngress(); var resultIngress = (existingIngress != null) ? existingIngress : defaultIngress; - if (resultIngress.getMetadata().getAnnotations() == null) { - resultIngress.getMetadata().setAnnotations(new HashMap<>()); - } - resultIngress.getMetadata().getAnnotations().putAll(defaultIngress.getMetadata().getAnnotations()); + resultIngress.getMetadata().setAnnotations(defaultIngress.getMetadata().getAnnotations()); resultIngress.setSpec(defaultIngress.getSpec()); return Optional.of(resultIngress); } @@ -64,15 +61,28 @@ public class KeycloakIngress extends OperatorManagedResource implements StatusUp private Ingress newIngress() { var port = KeycloakService.getServicePort(keycloak); - var backendProtocol = (!isTlsConfigured(keycloak)) ? "HTTP" : "HTTPS"; - var tlsTermination = "HTTP".equals(backendProtocol) ? "edge" : "passthrough"; + var annotations = new HashMap(); + + // set default annotations + if (isTlsConfigured(keycloak)) { + annotations.put("nginx.ingress.kubernetes.io/backend-protocol", "HTTPS"); + annotations.put("route.openshift.io/termination", "passthrough"); + } else { + annotations.put("nginx.ingress.kubernetes.io/backend-protocol", "HTTP"); + annotations.put("route.openshift.io/termination", "edge"); + } + + if (keycloak.getSpec().getIngressSpec() != null && + keycloak.getSpec().getIngressSpec().getAnnotations() != null) { + annotations.putAll(keycloak.getSpec().getIngressSpec().getAnnotations()); + + } Ingress ingress = new IngressBuilder() .withNewMetadata() .withName(getName()) .withNamespace(getNamespace()) - .addToAnnotations("nginx.ingress.kubernetes.io/backend-protocol", backendProtocol) - .addToAnnotations("route.openshift.io/termination", tlsTermination) + .addToAnnotations(annotations) .endMetadata() .withNewSpec() .withNewDefaultBackend() diff --git a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/IngressSpec.java b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/IngressSpec.java index 4247ab395f..d885b48151 100644 --- a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/IngressSpec.java +++ b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/IngressSpec.java @@ -18,14 +18,21 @@ package org.keycloak.operator.crds.v2alpha1.deployment.spec; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; import io.sundr.builder.annotations.Buildable; +import java.util.Map; + @Buildable(editableEnabled = false, builderPackage = "io.fabric8.kubernetes.api.builder") public class IngressSpec { @JsonProperty("enabled") private boolean enabled = true; + @JsonProperty("annotations") + @JsonPropertyDescription("Additional annotations to be appended to the Ingress object") + Map annotations; + public boolean isIngressEnabled() { return enabled; } @@ -33,4 +40,12 @@ public class IngressSpec { public void setIngressEnabled(boolean enabled) { this.enabled = enabled; } + + public Map getAnnotations() { + return annotations; + } + + public void setAnnotations(Map annotations) { + this.annotations = annotations; + } } diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakIngressTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakIngressTest.java index f900804f20..eac4bb931b 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakIngressTest.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakIngressTest.java @@ -39,6 +39,7 @@ import java.util.Map; import static java.util.concurrent.TimeUnit.MINUTES; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; @QuarkusTest public class KeycloakIngressTest extends BaseOperatorTest { @@ -171,6 +172,7 @@ public class KeycloakIngressTest extends BaseOperatorTest { var kc = K8sUtils.getDefaultKeycloakDeployment(); kc.getSpec().setIngressSpec(new IngressSpec()); kc.getSpec().getIngressSpec().setIngressEnabled(true); + kc.getSpec().getIngressSpec().setAnnotations(Map.of("haproxy.router.openshift.io/disable_cookies", "true")); K8sUtils.deployKeycloak(k8sclient, kc, true); var ingress = new KeycloakIngress(k8sclient, kc); @@ -206,6 +208,7 @@ public class KeycloakIngressTest extends BaseOperatorTest { 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("passthrough", i.getMetadata().getAnnotations().get("route.openshift.io/termination")); + assertEquals("true", i.getMetadata().getAnnotations().get("haproxy.router.openshift.io/disable_cookies")); assertEquals(Constants.KEYCLOAK_HTTPS_PORT, i.getSpec().getDefaultBackend().getService().getPort().getNumber()); }); @@ -267,6 +270,75 @@ public class KeycloakIngressTest extends BaseOperatorTest { } } + @Test + public void testCustomIngressAnnotations() { + var kc = K8sUtils.getDefaultKeycloakDeployment(); + kc.getSpec().setIngressSpec(new IngressSpec()); + kc.getSpec().getIngressSpec().setIngressEnabled(true); + + // set 'a' + kc.getSpec().getIngressSpec().setAnnotations(Map.of("a", "b")); + K8sUtils.deployKeycloak(k8sclient, kc, true); + + var ingress = new KeycloakIngress(k8sclient, kc); + var ingressSelector = k8sclient + .network() + .v1() + .ingresses() + .inNamespace(namespace) + .withName(ingress.getName()); + + Awaitility.await() + .ignoreExceptions() + .untilAsserted(() -> { + var i = ingressSelector.get(); + assertEquals("HTTPS", i.getMetadata().getAnnotations().get("nginx.ingress.kubernetes.io/backend-protocol")); + assertEquals("passthrough", i.getMetadata().getAnnotations().get("route.openshift.io/termination")); + assertEquals("b", i.getMetadata().getAnnotations().get("a")); + }); + + // update 'a' + kc.getSpec().getIngressSpec().setAnnotations(Map.of("a", "bb")); + K8sUtils.deployKeycloak(k8sclient, kc, true); + + Awaitility.await() + .ignoreExceptions() + .untilAsserted(() -> { + var i = ingressSelector.get(); + assertEquals("HTTPS", i.getMetadata().getAnnotations().get("nginx.ingress.kubernetes.io/backend-protocol")); + assertEquals("passthrough", i.getMetadata().getAnnotations().get("route.openshift.io/termination")); + assertEquals("bb", i.getMetadata().getAnnotations().get("a")); + }); + + // remove 'a' and add 'c' + kc.getSpec().getIngressSpec().setAnnotations(Map.of("c", "d")); + K8sUtils.deployKeycloak(k8sclient, kc, true); + + Awaitility.await() + .ignoreExceptions() + .untilAsserted(() -> { + var i = ingressSelector.get(); + assertEquals("HTTPS", i.getMetadata().getAnnotations().get("nginx.ingress.kubernetes.io/backend-protocol")); + assertEquals("passthrough", i.getMetadata().getAnnotations().get("route.openshift.io/termination")); + assertFalse(i.getMetadata().getAnnotations().containsKey("a")); + assertEquals("d", i.getMetadata().getAnnotations().get("c")); + }); + + // remove all + kc.getSpec().getIngressSpec().setAnnotations(null); + K8sUtils.deployKeycloak(k8sclient, kc, true); + + Awaitility.await() + .ignoreExceptions() + .untilAsserted(() -> { + var i = ingressSelector.get(); + assertEquals("HTTPS", i.getMetadata().getAnnotations().get("nginx.ingress.kubernetes.io/backend-protocol")); + assertEquals("passthrough", i.getMetadata().getAnnotations().get("route.openshift.io/termination")); + assertFalse(i.getMetadata().getAnnotations().containsKey("a")); + assertFalse(i.getMetadata().getAnnotations().containsKey("c")); + }); + } + private Ingress createCustomIngress(String baseResourceName, String targetNamespace, int portNumber) { Ingress customIngressCreated; diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/unit/CRSerializationTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/unit/CRSerializationTest.java index dc6ee091f6..7f19c6e43c 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/unit/CRSerializationTest.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/unit/CRSerializationTest.java @@ -24,10 +24,11 @@ import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret; import org.keycloak.operator.crds.v2alpha1.deployment.spec.DatabaseSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.FeatureSpec; -import org.keycloak.operator.crds.v2alpha1.deployment.spec.TransactionsSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.HostnameSpec; +import org.keycloak.operator.crds.v2alpha1.deployment.spec.TransactionsSpec; import java.util.List; +import java.util.Map; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasItem; @@ -41,6 +42,11 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; public class CRSerializationTest { + private static final Map CUSTOM_INGRESS_ANNOTATION = Map.of( + "myAnnotation", "myValue", + "anotherAnnotation", "anotherValue" + ); + @Test public void testDeserialization() { Keycloak keycloak = Serialization.unmarshal(this.getClass().getResourceAsStream("/test-serialization-keycloak-cr.yml"), Keycloak.class); @@ -49,6 +55,7 @@ public class CRSerializationTest { assertEquals("my-image", keycloak.getSpec().getImage()); assertEquals("my-tls-secret", keycloak.getSpec().getHttpSpec().getTlsSecret()); assertFalse(keycloak.getSpec().getIngressSpec().isIngressEnabled()); + assertEquals(CUSTOM_INGRESS_ANNOTATION, keycloak.getSpec().getIngressSpec().getAnnotations()); final TransactionsSpec transactionsSpec = keycloak.getSpec().getTransactionsSpec(); assertThat(transactionsSpec, notNullValue()); diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/unit/IngressLogicTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/unit/IngressLogicTest.java index 8a691d7fbf..56b9cf4fc9 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/unit/IngressLogicTest.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/unit/IngressLogicTest.java @@ -18,12 +18,12 @@ package org.keycloak.operator.testsuite.unit; import java.util.Collections; +import java.util.Map; 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.keycloak.operator.Constants; import org.keycloak.operator.controllers.KeycloakIngress; import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; import org.keycloak.operator.crds.v2alpha1.deployment.spec.IngressSpec; @@ -39,14 +39,19 @@ import static org.junit.jupiter.api.Assertions.assertTrue; public class IngressLogicTest { + private static final String EXISTING_ANNOTATION_KEY = "annotation"; + static class MockKeycloakIngress extends KeycloakIngress { - private static Keycloak getKeycloak(Boolean defaultIngressEnabled, boolean ingressSpecDefined, boolean tlsConfigured) { + private static Keycloak getKeycloak(Boolean defaultIngressEnabled, boolean ingressSpecDefined, boolean tlsConfigured, Map annotations) { var kc = K8sUtils.getDefaultKeycloakDeployment(); kc.getMetadata().setUid("this-is-a-fake-uid"); if (ingressSpecDefined) { kc.getSpec().setIngressSpec(new IngressSpec()); if (defaultIngressEnabled != null) kc.getSpec().getIngressSpec().setIngressEnabled(defaultIngressEnabled); + if (annotations != null) { + kc.getSpec().getIngressSpec().setAnnotations(annotations); + } } if (!tlsConfigured) { kc.getSpec().getHttpSpec().setTlsSecret(null); @@ -59,14 +64,18 @@ public class IngressLogicTest { } public static MockKeycloakIngress build(Boolean defaultIngressEnabled, boolean ingressExists, boolean ingressSpecDefined, boolean tlsConfigured) { + return build(defaultIngressEnabled, ingressExists, ingressSpecDefined, tlsConfigured, null); + } + + public static MockKeycloakIngress build(Boolean defaultIngressEnabled, boolean ingressExists, boolean ingressSpecDefined, boolean tlsConfigured, Map annotations) { MockKeycloakIngress.ingressExists = ingressExists; - return new MockKeycloakIngress(defaultIngressEnabled, ingressSpecDefined, tlsConfigured); + return new MockKeycloakIngress(defaultIngressEnabled, ingressSpecDefined, tlsConfigured, annotations); } public static boolean ingressExists = false; private boolean deleted = false; - public MockKeycloakIngress(Boolean defaultIngressEnabled, boolean ingressSpecDefined, boolean tlsConfigured) { - super(null, getKeycloak(defaultIngressEnabled, ingressSpecDefined, tlsConfigured)); + public MockKeycloakIngress(Boolean defaultIngressEnabled, boolean ingressSpecDefined, boolean tlsConfigured, Map annotations) { + super(null, getKeycloak(defaultIngressEnabled, ingressSpecDefined, tlsConfigured, annotations)); } @Override @@ -100,6 +109,7 @@ public class IngressLogicTest { .withName(getName()) .withNamespace(cr.getMetadata().getNamespace()) .withOwnerReferences(Collections.singletonList(sameCROwnerRef)) + .withAnnotations(Map.of(EXISTING_ANNOTATION_KEY, "value")) .endMetadata() .build(); } else { @@ -174,4 +184,38 @@ public class IngressLogicTest { assertEquals("HTTP", reconciled.get().getMetadata().getAnnotations().get("nginx.ingress.kubernetes.io/backend-protocol")); assertEquals("edge", reconciled.get().getMetadata().getAnnotations().get("route.openshift.io/termination")); } + + @Test + public void testCustomAnnotations() { + var kc = MockKeycloakIngress.build(null, false, true, true, Map.of("custom", "value")); + Optional reconciled = kc.getReconciledResource(); + assertTrue(reconciled.isPresent()); + assertFalse(kc.deleted()); + assertEquals("HTTPS", reconciled.get().getMetadata().getAnnotations().get("nginx.ingress.kubernetes.io/backend-protocol")); + assertEquals("passthrough", reconciled.get().getMetadata().getAnnotations().get("route.openshift.io/termination")); + assertEquals("value", reconciled.get().getMetadata().getAnnotations().get("custom")); + assertFalse(reconciled.get().getMetadata().getAnnotations().containsKey(EXISTING_ANNOTATION_KEY)); + } + + @Test + public void testRemoveCustomAnnotation() { + var kc = MockKeycloakIngress.build(null, true, true, true, null); + Optional reconciled = kc.getReconciledResource(); + assertTrue(reconciled.isPresent()); + assertFalse(kc.deleted()); + assertEquals("HTTPS", reconciled.get().getMetadata().getAnnotations().get("nginx.ingress.kubernetes.io/backend-protocol")); + assertEquals("passthrough", reconciled.get().getMetadata().getAnnotations().get("route.openshift.io/termination")); + assertFalse(reconciled.get().getMetadata().getAnnotations().containsKey(EXISTING_ANNOTATION_KEY)); + } + + @Test + public void testUpdateCustomAnnotation() { + var kc = MockKeycloakIngress.build(null, true, true, true, Map.of(EXISTING_ANNOTATION_KEY, "another-value")); + Optional reconciled = kc.getReconciledResource(); + assertTrue(reconciled.isPresent()); + assertFalse(kc.deleted()); + assertEquals("HTTPS", reconciled.get().getMetadata().getAnnotations().get("nginx.ingress.kubernetes.io/backend-protocol")); + assertEquals("passthrough", reconciled.get().getMetadata().getAnnotations().get("route.openshift.io/termination")); + assertEquals("another-value", reconciled.get().getMetadata().getAnnotations().get(EXISTING_ANNOTATION_KEY)); + } } diff --git a/operator/src/test/resources/test-serialization-keycloak-cr.yml b/operator/src/test/resources/test-serialization-keycloak-cr.yml index a5470f42d1..cc2a43810b 100644 --- a/operator/src/test/resources/test-serialization-keycloak-cr.yml +++ b/operator/src/test/resources/test-serialization-keycloak-cr.yml @@ -28,6 +28,9 @@ spec: poolMaxSize: 3 ingress: enabled: false + annotations: + myAnnotation: myValue + anotherAnnotation: anotherValue http: httpEnabled: true httpPort: 123