Allow custom annotation in Ingress (#20577)

Closes #20576
This commit is contained in:
Pedro Ruivo 2023-05-26 16:24:59 +01:00 committed by GitHub
parent b438776b94
commit cffb8141e2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 165 additions and 14 deletions

View file

@ -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<String, String>();
// 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()

View file

@ -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<String, String> annotations;
public boolean isIngressEnabled() {
return enabled;
}
@ -33,4 +40,12 @@ public class IngressSpec {
public void setIngressEnabled(boolean enabled) {
this.enabled = enabled;
}
public Map<String, String> getAnnotations() {
return annotations;
}
public void setAnnotations(Map<String, String> annotations) {
this.annotations = annotations;
}
}

View file

@ -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;

View file

@ -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<String, String> 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());

View file

@ -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<String, String> 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<String, String> 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<String, String> 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<HasMetadata> 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<HasMetadata> 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<HasMetadata> 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));
}
}

View file

@ -28,6 +28,9 @@ spec:
poolMaxSize: 3
ingress:
enabled: false
annotations:
myAnnotation: myValue
anotherAnnotation: anotherValue
http:
httpEnabled: true
httpPort: 123