diff --git a/docs/guides/operator/advanced-configuration.adoc b/docs/guides/operator/advanced-configuration.adoc index 5e274d01357..7c94cc3a36d 100644 --- a/docs/guides/operator/advanced-configuration.adoc +++ b/docs/guides/operator/advanced-configuration.adoc @@ -462,4 +462,25 @@ They need to access {project_name} to scrape the available metrics. Check the https://kubernetes.io/docs/concepts/services-networking/network-policies/[Kubernetes Network Policies documentation] for more information about NetworkPolicies. +=== Parameterizing service labels and annotations + +If you need to set custom labels or annotations to keycloak service you can do that through `spec.http.labels` and `spec.http.annotations` + +.Custom service labels and annotations +[source,yaml] +---- +apiVersion: k8s.keycloak.org/v2alpha1 +kind: Keycloak +metadata: + name: example-kc +spec: + http: + labels: + label1: label-value1 + label2: label-value2 + annotations: + annotation1: annotation-value1 + annotation2: annotation-value2 +---- + diff --git a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakServiceDependentResource.java b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakServiceDependentResource.java index 7ed3549d2a3..1b9270fb4dc 100644 --- a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakServiceDependentResource.java +++ b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakServiceDependentResource.java @@ -17,6 +17,9 @@ package org.keycloak.operator.controllers; import java.util.Optional; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.Service; @@ -76,11 +79,19 @@ public class KeycloakServiceDependentResource extends CRUDKubernetesDependentRes @Override protected Service desired(Keycloak primary, Context context) { + + Map labels = Utils.allInstanceLabels(primary); + var optionalSpec = Optional.ofNullable(primary.getSpec().getHttpSpec()); + optionalSpec.map(HttpSpec::getLabels).ifPresent(labels::putAll); + + Map annotations = optionalSpec.map(HttpSpec::getAnnotations).orElse(new HashMap<>()); + Service service = new ServiceBuilder() .withNewMetadata() .withName(getServiceName(primary)) .withNamespace(primary.getMetadata().getNamespace()) - .addToLabels(Utils.allInstanceLabels(primary)) + .addToLabels(labels) + .addToAnnotations(annotations) .endMetadata() .withSpec(getServiceSpec(primary)) .build(); diff --git a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/HttpSpec.java b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/HttpSpec.java index 1cd6a12c8f2..263b1e004f9 100644 --- a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/HttpSpec.java +++ b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/HttpSpec.java @@ -18,11 +18,14 @@ package org.keycloak.operator.crds.v2alpha1.deployment.spec; import java.util.Optional; +import java.util.Map; + +import org.keycloak.operator.Constants; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonPropertyDescription; + import io.sundr.builder.annotations.Buildable; -import org.keycloak.operator.Constants; import org.keycloak.operator.crds.v2alpha1.CRDUtils; import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakSpec; @@ -45,6 +48,12 @@ public class HttpSpec { @JsonPropertyDescription("The used HTTPS port.") private Integer httpsPort = Constants.KEYCLOAK_HTTPS_PORT; + @JsonPropertyDescription("Annotations to be appended to the Service object") + Map annotations; + + @JsonPropertyDescription("Labels to be appended to the Service object") + Map labels; + public String getTlsSecret() { return tlsSecret; } @@ -95,4 +104,20 @@ public class HttpSpec { .map(KeycloakSpec::getHttpSpec); } + public Map getAnnotations() { + return annotations; + } + + public void setAnnotations(Map annotations) { + this.annotations = annotations; + } + + public Map getLabels() { + return labels; + } + + public void setLabels(Map labels) { + this.labels = labels; + } + } diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakServicesTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakServicesTest.java index 0697a51bc42..03b9ac93997 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakServicesTest.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakServicesTest.java @@ -31,12 +31,15 @@ import java.time.Duration; import java.util.Map; 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 KeycloakServicesTest extends BaseOperatorTest { @Test public void testMainServiceDurability() { var kc = getTestKeycloakDeployment(true); + kc.getSpec().getHttpSpec().setLabels(Map.of("foo","bar")); K8sUtils.deployKeycloak(k8sclient, kc, true); String serviceName = KeycloakServiceDependentResource.getServiceName(kc); var serviceSelector = k8sclient.services().inNamespace(namespace).withName(serviceName); @@ -71,6 +74,7 @@ public class KeycloakServicesTest extends BaseOperatorTest { .untilAsserted(() -> { var s = serviceSelector.get(); assertThat(s.getMetadata().getLabels().entrySet().containsAll(labels.entrySet())).isTrue(); // additional labels should not be overwritten + assertEquals("bar", s.getMetadata().getLabels().get("foo")); // ignoring assigned IP/s and generated config s.getSpec().setClusterIP(null); s.getSpec().setClusterIPs(null); @@ -79,6 +83,60 @@ public class KeycloakServicesTest extends BaseOperatorTest { }); } + @Test + public void testCustomServiceAnnotations() { + var kc = getTestKeycloakDeployment(true); + + // set 'a' + kc.getSpec().getHttpSpec().setAnnotations(Map.of("a", "b")); + K8sUtils.deployKeycloak(k8sclient, kc, true); + + String serviceName = KeycloakServiceDependentResource.getServiceName(kc); + var serviceSelector = k8sclient.services().inNamespace(namespace).withName(serviceName); + + Awaitility.await() + .ignoreExceptions() + .untilAsserted(() -> { + var s = serviceSelector.get(); + assertEquals("b", s.getMetadata().getAnnotations().get("a")); + }); + + // update 'a' + kc.getSpec().getHttpSpec().setAnnotations(Map.of("a", "bb")); + K8sUtils.deployKeycloak(k8sclient, kc, true); + + Awaitility.await() + .ignoreExceptions() + .untilAsserted(() -> { + var s = serviceSelector.get(); + assertEquals("bb", s.getMetadata().getAnnotations().get("a")); + }); + + // remove 'a' and add 'c' + kc.getSpec().getHttpSpec().setAnnotations(Map.of("c", "d")); + K8sUtils.deployKeycloak(k8sclient, kc, true); + + Awaitility.await() + .ignoreExceptions() + .untilAsserted(() -> { + var s = serviceSelector.get(); + assertFalse(s.getMetadata().getAnnotations().containsKey("a")); + assertEquals("d", s.getMetadata().getAnnotations().get("c")); + }); + + // remove all + kc.getSpec().getHttpSpec().setAnnotations(null); + K8sUtils.deployKeycloak(k8sclient, kc, true); + + Awaitility.await() + .ignoreExceptions() + .untilAsserted(() -> { + var s = serviceSelector.get(); + assertFalse(s.getMetadata().getAnnotations().containsKey("a")); + assertFalse(s.getMetadata().getAnnotations().containsKey("c")); + }); + } + @Test public void testDiscoveryServiceDurability() { var kc = getTestKeycloakDeployment(true);