Add hostname options to Keycloak CR

Closes #14395

Co-authored-by: Václav Muzikář <vmuzikar@redhat.com>
This commit is contained in:
Martin Bartoš 2022-10-19 12:10:04 +02:00 committed by Václav Muzikář
parent acaf1724dd
commit 24acc4c7d1
15 changed files with 321 additions and 174 deletions

View file

@ -33,6 +33,7 @@ import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusBuilder;
import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret; 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.DatabaseSpec;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.FeatureSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.FeatureSpec;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.HostnameSpec;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.HttpSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.HttpSpec;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.TransactionsSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.TransactionsSpec;
@ -91,30 +92,12 @@ public class KeycloakDistConfigurator {
/* ---------- Configuration of first-class citizen fields ---------- */ /* ---------- Configuration of first-class citizen fields ---------- */
public void configureHostname() { public void configureHostname() {
var kcContainer = deployment.getSpec().getTemplate().getSpec().getContainers().get(0); optionMapper(keycloakCR.getSpec().getHostnameSpec())
var hostname = keycloakCR.getSpec().getHostname(); .mapOption("hostname", HostnameSpec::getHostname)
var envVars = kcContainer.getEnv(); .mapOption("hostname-admin", HostnameSpec::getAdmin)
if (keycloakCR.getSpec().isHostnameDisabled()) { .mapOption("hostname-admin-url", HostnameSpec::getAdminUrl)
var disableStrictHostname = List.of( .mapOption("hostname-strict", HostnameSpec::isStrict)
new EnvVarBuilder() .mapOption("hostname-strict-backchannel", HostnameSpec::isStrictBackchannel);
.withName("KC_HOSTNAME_STRICT")
.withValue("false")
.build(),
new EnvVarBuilder()
.withName("KC_HOSTNAME_STRICT_BACKCHANNEL")
.withValue("false")
.build());
envVars.addAll(disableStrictHostname);
} else {
var enabledStrictHostname = List.of(
new EnvVarBuilder()
.withName("KC_HOSTNAME")
.withValue(hostname)
.build());
envVars.addAll(enabledStrictHostname);
}
} }
public void configureFeatures() { public void configureFeatures() {

View file

@ -101,8 +101,9 @@ public class KeycloakIngress extends OperatorManagedResource implements StatusUp
.endSpec() .endSpec()
.build(); .build();
if (!keycloak.getSpec().isHostnameDisabled()) { final var hostnameSpec = keycloak.getSpec().getHostnameSpec();
ingress.getSpec().getRules().get(0).setHost(keycloak.getSpec().getHostname()); if (hostnameSpec != null && hostnameSpec.getHostname() != null) {
ingress.getSpec().getRules().get(0).setHost(hostnameSpec.getHostname());
} }
return ingress; return ingress;

View file

@ -16,20 +16,18 @@
*/ */
package org.keycloak.operator.crds.v2alpha1.deployment; package org.keycloak.operator.crds.v2alpha1.deployment;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription; import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import io.fabric8.kubernetes.api.model.LocalObjectReference; import io.fabric8.kubernetes.api.model.LocalObjectReference;
import org.keycloak.operator.Constants;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.DatabaseSpec; 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.FeatureSpec;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.HttpSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.HttpSpec;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.UnsupportedSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.UnsupportedSpec;
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.TransactionsSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.TransactionsSpec;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.HostnameSpec;
import javax.validation.constraints.NotNull;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -48,11 +46,6 @@ public class KeycloakSpec {
"expressed as a keys (reference: https://www.keycloak.org/server/all-config) and values that can be either direct values or references to secrets.") "expressed as a keys (reference: https://www.keycloak.org/server/all-config) and values that can be either direct values or references to secrets.")
private List<ValueOrSecret> serverConfiguration; // can't use Set due to a bug in Sundrio https://github.com/sundrio/sundrio/issues/316 private List<ValueOrSecret> serverConfiguration; // can't use Set due to a bug in Sundrio https://github.com/sundrio/sundrio/issues/316
@NotNull
@JsonPropertyDescription("Hostname for the Keycloak server.\n" +
"The special value `" + Constants.INSECURE_DISABLE + "` disables the hostname strict resolution.")
private String hostname;
@JsonProperty("http") @JsonProperty("http")
@JsonPropertyDescription("In this section you can configure Keycloak features related to HTTP and HTTPS") @JsonPropertyDescription("In this section you can configure Keycloak features related to HTTP and HTTPS")
private HttpSpec httpSpec; private HttpSpec httpSpec;
@ -79,18 +72,9 @@ public class KeycloakSpec {
@JsonPropertyDescription("In this section you can find all properties related to connect to a database.") @JsonPropertyDescription("In this section you can find all properties related to connect to a database.")
private DatabaseSpec databaseSpec; private DatabaseSpec databaseSpec;
public String getHostname() { @JsonProperty("hostname")
return hostname; @JsonPropertyDescription("In this section you can configure Keycloak hostname and related properties.")
} private HostnameSpec hostnameSpec;
public void setHostname(String hostname) {
this.hostname = hostname;
}
@JsonIgnore
public boolean isHostnameDisabled() {
return this.hostname.equals(Constants.INSECURE_DISABLE);
}
public HttpSpec getHttpSpec() { public HttpSpec getHttpSpec() {
return httpSpec; return httpSpec;
@ -140,6 +124,14 @@ public class KeycloakSpec {
this.databaseSpec = databaseSpec; this.databaseSpec = databaseSpec;
} }
public HostnameSpec getHostnameSpec() {
return hostnameSpec;
}
public void setHostnameSpec(HostnameSpec hostnameSpec) {
this.hostnameSpec = hostnameSpec;
}
public int getInstances() { public int getInstances() {
return instances; return instances;
} }

View file

@ -0,0 +1,82 @@
/*
* Copyright 2022 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.crds.v2alpha1.deployment.spec;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import io.sundr.builder.annotations.Buildable;
import java.io.Serializable;
@Buildable(editableEnabled = false, builderPackage = "io.fabric8.kubernetes.api.builder")
public class HostnameSpec implements Serializable {
@JsonPropertyDescription("Hostname for the Keycloak server.")
private String hostname;
@JsonPropertyDescription("The hostname for accessing the administration console.")
private String admin;
@JsonPropertyDescription("Set the base URL for accessing the administration console, including scheme, host, port and path")
private String adminUrl;
@JsonPropertyDescription("Disables dynamically resolving the hostname from request headers.")
private Boolean strict;
@JsonPropertyDescription("By default backchannel URLs are dynamically resolved from request headers to allow internal and external applications.")
private Boolean strictBackchannel;
public String getHostname() {
return hostname;
}
public void setHostname(String hostname) {
this.hostname = hostname;
}
public String getAdmin() {
return admin;
}
public void setAdmin(String admin) {
this.admin = admin;
}
public String getAdminUrl() {
return adminUrl;
}
public void setAdminUrl(String adminUrl) {
this.adminUrl = adminUrl;
}
public Boolean isStrict() {
return strict;
}
public void setStrict(Boolean strict) {
this.strict = strict;
}
public Boolean isStrictBackchannel() {
return strictBackchannel;
}
public void setStrictBackchannel(Boolean strictBackchannel) {
this.strictBackchannel = strictBackchannel;
}
}

View file

@ -13,6 +13,7 @@ spec:
passwordSecret: passwordSecret:
name: keycloak-db-secret name: keycloak-db-secret
key: password key: password
hostname: example.com
http: http:
tlsSecret: example-tls-secret tlsSecret: example-tls-secret
hostname:
hostname: example.com

View file

@ -30,13 +30,14 @@ import org.awaitility.Awaitility;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty; import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import org.keycloak.operator.Constants; import org.keycloak.operator.Constants;
import org.keycloak.operator.controllers.KeycloakDistConfigurator;
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusCondition;
import org.keycloak.operator.testsuite.utils.K8sUtils;
import org.keycloak.operator.controllers.KeycloakAdminSecret; import org.keycloak.operator.controllers.KeycloakAdminSecret;
import org.keycloak.operator.controllers.KeycloakDistConfigurator;
import org.keycloak.operator.controllers.KeycloakService; import org.keycloak.operator.controllers.KeycloakService;
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusCondition;
import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret; import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.HostnameSpecBuilder;
import org.keycloak.operator.testsuite.utils.K8sUtils;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.time.Duration; import java.time.Duration;
@ -257,7 +258,12 @@ public class KeycloakDeploymentTest extends BaseOperatorTest {
public void testHostnameStrictDisabled() { public void testHostnameStrictDisabled() {
try { try {
var kc = getDefaultKeycloakDeployment(); var kc = getDefaultKeycloakDeployment();
kc.getSpec().setHostname(Constants.INSECURE_DISABLE); var hostnameSpec = new HostnameSpecBuilder()
.withStrict(false)
.withStrictBackchannel(false)
.build();
kc.getSpec().setHostnameSpec(hostnameSpec);
deployKeycloak(k8sclient, kc, true); deployKeycloak(k8sclient, kc, true);
var service = new KeycloakService(k8sclient, kc); var service = new KeycloakService(k8sclient, kc);
@ -284,9 +290,15 @@ public class KeycloakDeploymentTest extends BaseOperatorTest {
final int httpsPort = 8543; final int httpsPort = 8543;
final int httpPort = 8180; final int httpPort = 8180;
var kc = getDefaultKeycloakDeployment(); var kc = getDefaultKeycloakDeployment();
kc.getSpec().setHostname(Constants.INSECURE_DISABLE);
kc.getSpec().getHttpSpec().setHttpsPort(httpsPort); kc.getSpec().getHttpSpec().setHttpsPort(httpsPort);
kc.getSpec().getHttpSpec().setHttpPort(httpPort); kc.getSpec().getHttpSpec().setHttpPort(httpPort);
var hostnameSpec = new HostnameSpecBuilder()
.withStrict(false)
.withStrictBackchannel(false)
.build();
kc.getSpec().setHostnameSpec(hostnameSpec);
deployKeycloak(k8sclient, kc, true); deployKeycloak(k8sclient, kc, true);
assertKeycloakAccessibleViaService(kc, true, httpsPort); assertKeycloakAccessibleViaService(kc, true, httpsPort);
@ -302,11 +314,17 @@ public class KeycloakDeploymentTest extends BaseOperatorTest {
final int httpsPort = 8543; final int httpsPort = 8543;
final int httpPort = 8180; final int httpPort = 8180;
var kc = getDefaultKeycloakDeployment(); var kc = getDefaultKeycloakDeployment();
kc.getSpec().setHostname(Constants.INSECURE_DISABLE);
kc.getSpec().getHttpSpec().setHttpsPort(httpsPort); kc.getSpec().getHttpSpec().setHttpsPort(httpsPort);
kc.getSpec().getHttpSpec().setHttpPort(httpPort); kc.getSpec().getHttpSpec().setHttpPort(httpPort);
kc.getSpec().getHttpSpec().setTlsSecret(null); kc.getSpec().getHttpSpec().setTlsSecret(null);
kc.getSpec().getHttpSpec().setHttpEnabled(true); kc.getSpec().getHttpSpec().setHttpEnabled(true);
var hostnameSpec = new HostnameSpecBuilder()
.withStrict(false)
.withStrictBackchannel(false)
.build();
kc.getSpec().setHostnameSpec(hostnameSpec);
deployKeycloak(k8sclient, kc, true); deployKeycloak(k8sclient, kc, true);
assertKeycloakAccessibleViaService(kc, false, httpPort); assertKeycloakAccessibleViaService(kc, false, httpPort);

View file

@ -25,6 +25,7 @@ import org.awaitility.Awaitility;
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.crds.v2alpha1.deployment.spec.IngressSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.IngressSpec;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.HostnameSpecBuilder;
import org.keycloak.operator.testsuite.utils.K8sUtils; import org.keycloak.operator.testsuite.utils.K8sUtils;
import org.keycloak.operator.controllers.KeycloakIngress; import org.keycloak.operator.controllers.KeycloakIngress;
@ -39,9 +40,13 @@ public class KeycloakIngressTest extends BaseOperatorTest {
@Test @Test
public void testIngressOnHTTP() { public void testIngressOnHTTP() {
var kc = K8sUtils.getDefaultKeycloakDeployment(); var kc = K8sUtils.getDefaultKeycloakDeployment();
kc.getSpec().setHostname(Constants.INSECURE_DISABLE);
kc.getSpec().getHttpSpec().setTlsSecret(null); kc.getSpec().getHttpSpec().setTlsSecret(null);
kc.getSpec().getHttpSpec().setHttpEnabled(true); kc.getSpec().getHttpSpec().setHttpEnabled(true);
var hostnameSpec = new HostnameSpecBuilder()
.withStrict(false)
.withStrictBackchannel(false)
.build();
kc.getSpec().setHostnameSpec(hostnameSpec);
K8sUtils.deployKeycloak(k8sclient, kc, true); K8sUtils.deployKeycloak(k8sclient, kc, true);
Awaitility.await() Awaitility.await()
@ -70,7 +75,12 @@ public class KeycloakIngressTest extends BaseOperatorTest {
@Test @Test
public void testIngressOnHTTPS() { public void testIngressOnHTTPS() {
var kc = K8sUtils.getDefaultKeycloakDeployment(); var kc = K8sUtils.getDefaultKeycloakDeployment();
kc.getSpec().setHostname(Constants.INSECURE_DISABLE); var hostnameSpec = new HostnameSpecBuilder()
.withStrict(false)
.withStrictBackchannel(false)
.build();
kc.getSpec().setHostnameSpec(hostnameSpec);
K8sUtils.deployKeycloak(k8sclient, kc, true); K8sUtils.deployKeycloak(k8sclient, kc, true);
Awaitility.await() Awaitility.await()
@ -101,7 +111,9 @@ public class KeycloakIngressTest extends BaseOperatorTest {
@Test @Test
public void testIngressHostname() { public void testIngressHostname() {
var kc = K8sUtils.getDefaultKeycloakDeployment(); var kc = K8sUtils.getDefaultKeycloakDeployment();
kc.getSpec().setHostname("foo.bar"); var hostnameSpec = new HostnameSpecBuilder().withHostname("foo.bar").build();
kc.getSpec().setHostnameSpec(hostnameSpec);
K8sUtils.deployKeycloak(k8sclient, kc, true); K8sUtils.deployKeycloak(k8sclient, kc, true);
var ingress = new KeycloakIngress(k8sclient, kc); var ingress = new KeycloakIngress(k8sclient, kc);

View file

@ -28,6 +28,7 @@ import org.keycloak.operator.controllers.WatchedSecretsStore;
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusCondition; import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusCondition;
import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret; import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.HostnameSpecBuilder;
import java.util.Base64; import java.util.Base64;
import java.util.HashSet; import java.util.HashSet;
@ -161,12 +162,14 @@ public class WatchedSecretsTest extends BaseOperatorTest {
public void testSingleSecretMultipleKeycloaks() { public void testSingleSecretMultipleKeycloaks() {
try { try {
var kc1 = getDefaultKeycloakDeployment(); var kc1 = getDefaultKeycloakDeployment();
var kc1Hostname = new HostnameSpecBuilder().withHostname("kc1.local").build();
kc1.getMetadata().setName(kc1.getMetadata().getName() + "-1"); kc1.getMetadata().setName(kc1.getMetadata().getName() + "-1");
kc1.getSpec().setHostname("kc1.local"); kc1.getSpec().setHostnameSpec(kc1Hostname);
var kc2 = getDefaultKeycloakDeployment(); var kc2 = getDefaultKeycloakDeployment();
var kc2Hostname = new HostnameSpecBuilder().withHostname("kc2.local").build();
kc2.getMetadata().setName(kc2.getMetadata().getName() + "-2"); kc2.getMetadata().setName(kc2.getMetadata().getName() + "-2");
kc2.getSpec().setHostname("kc2.local"); // to prevent Ingress conflicts kc2.getSpec().setHostnameSpec(kc2Hostname); // to prevent Ingress conflicts
deployKeycloak(k8sclient, kc1, true); deployKeycloak(k8sclient, kc1, true);
deployKeycloak(k8sclient, kc2, true); deployKeycloak(k8sclient, kc2, true);

View file

@ -19,13 +19,13 @@ package org.keycloak.operator.testsuite.unit;
import io.fabric8.kubernetes.client.utils.Serialization; import io.fabric8.kubernetes.client.utils.Serialization;
import org.hamcrest.CoreMatchers; import org.hamcrest.CoreMatchers;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret; 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.DatabaseSpec;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.FeatureSpec; 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.TransactionsSpec;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.HostnameSpec;
import java.util.List; import java.util.List;
@ -34,6 +34,7 @@ import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasProperty; import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
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;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
@ -44,7 +45,7 @@ public class CRSerializationTest {
public void testDeserialization() { public void testDeserialization() {
Keycloak keycloak = Serialization.unmarshal(this.getClass().getResourceAsStream("/test-serialization-keycloak-cr.yml"), Keycloak.class); Keycloak keycloak = Serialization.unmarshal(this.getClass().getResourceAsStream("/test-serialization-keycloak-cr.yml"), Keycloak.class);
assertEquals("my-hostname", keycloak.getSpec().getHostname()); assertEquals("my-hostname", keycloak.getSpec().getHostnameSpec().getHostname());
assertEquals("my-image", keycloak.getSpec().getImage()); assertEquals("my-image", keycloak.getSpec().getImage());
assertEquals("my-tls-secret", keycloak.getSpec().getHttpSpec().getTlsSecret()); assertEquals("my-tls-secret", keycloak.getSpec().getHttpSpec().getTlsSecret());
assertFalse(keycloak.getSpec().getIngressSpec().isIngressEnabled()); assertFalse(keycloak.getSpec().getIngressSpec().isIngressEnabled());
@ -78,7 +79,7 @@ public class CRSerializationTest {
} }
@Test @Test
public void featureSpecificationDeserialization(){ public void featureSpecification() {
Keycloak keycloak = Serialization.unmarshal(this.getClass().getResourceAsStream("/test-serialization-keycloak-cr.yml"), Keycloak.class); Keycloak keycloak = Serialization.unmarshal(this.getClass().getResourceAsStream("/test-serialization-keycloak-cr.yml"), Keycloak.class);
final FeatureSpec featureSpec = keycloak.getSpec().getFeatureSpec(); final FeatureSpec featureSpec = keycloak.getSpec().getFeatureSpec();
@ -95,4 +96,27 @@ public class CRSerializationTest {
assertThat(disabledFeatures.get(1), CoreMatchers.is("step-up-authentication")); assertThat(disabledFeatures.get(1), CoreMatchers.is("step-up-authentication"));
} }
@Test
public void hostnameSpecification() {
Keycloak keycloak = Serialization.unmarshal(this.getClass().getResourceAsStream("/test-serialization-keycloak-cr.yml"), Keycloak.class);
HostnameSpec hostnameSpec = keycloak.getSpec().getHostnameSpec();
assertThat(hostnameSpec, notNullValue());
assertThat(hostnameSpec.getHostname(), is("my-hostname"));
assertThat(hostnameSpec.getAdmin(), is("my-admin-hostname"));
assertThat(hostnameSpec.getAdminUrl(), is("https://www.my-admin-hostname.org:8448/something"));
assertThat(hostnameSpec.isStrict(), is(true));
assertThat(hostnameSpec.isStrictBackchannel(), is(true));
keycloak = Serialization.unmarshal(this.getClass().getResourceAsStream("/empty-podtemplate-keycloak.yml"), Keycloak.class);
hostnameSpec = keycloak.getSpec().getHostnameSpec();
assertThat(hostnameSpec, notNullValue());
assertThat(hostnameSpec.getHostname(), is("example.com"));
assertThat(hostnameSpec.getAdmin(), nullValue());
assertThat(hostnameSpec.getAdminUrl(), nullValue());
assertThat(hostnameSpec.isStrict(), nullValue());
assertThat(hostnameSpec.isStrictBackchannel(), nullValue());
}
} }

View file

@ -24,7 +24,6 @@ import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder;
import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.keycloak.common.util.CollectionUtil; import org.keycloak.common.util.CollectionUtil;
import org.keycloak.common.util.ObjectUtil;
import org.keycloak.operator.Constants; import org.keycloak.operator.Constants;
import org.keycloak.operator.controllers.KeycloakDistConfigurator; import org.keycloak.operator.controllers.KeycloakDistConfigurator;
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
@ -34,12 +33,17 @@ import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusCondition;
import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret; import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret;
import org.keycloak.operator.testsuite.utils.K8sUtils; import org.keycloak.operator.testsuite.utils.K8sUtils;
import java.util.Collections; import java.util.Collection;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function; import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.keycloak.common.util.ObjectUtil.isBlank;
import static org.keycloak.operator.controllers.KeycloakDistConfigurator.getKeycloakOptionEnvVarName;
import static org.keycloak.operator.testsuite.utils.CRAssert.assertKeycloakStatusCondition; import static org.keycloak.operator.testsuite.utils.CRAssert.assertKeycloakStatusCondition;
import static org.keycloak.operator.testsuite.utils.CRAssert.assertKeycloakStatusDoesNotContainMessage; import static org.keycloak.operator.testsuite.utils.CRAssert.assertKeycloakStatusDoesNotContainMessage;
@ -48,50 +52,34 @@ public class KeycloakDistConfiguratorTest {
@Test @Test
public void enabledFeatures() { public void enabledFeatures() {
testFirstClassCitizen("KC_FEATURES", "features", testFirstClassCitizen(Map.of("features", "docker,authorization"), KeycloakDistConfigurator::configureFeatures);
KeycloakDistConfigurator::configureFeatures, "docker", "authorization");
} }
@Test @Test
public void disabledFeatures() { public void disabledFeatures() {
testFirstClassCitizen("KC_FEATURES_DISABLED", "features-disabled", testFirstClassCitizen(Map.of("features-disabled", "admin,step-up-authentication"), KeycloakDistConfigurator::configureFeatures);
KeycloakDistConfigurator::configureFeatures, "admin", "step-up-authentication");
} }
@Test @Test
public void transactions() { public void transactions() {
testFirstClassCitizen("KC_TRANSACTION_XA_ENABLED", "transaction-xa-enabled", testFirstClassCitizen(Map.of("transaction-xa-enabled", "false"), KeycloakDistConfigurator::configureTransactions);
KeycloakDistConfigurator::configureTransactions, "false");
} }
@Test @Test
public void httpEnabled() { public void http() {
testFirstClassCitizen("KC_HTTP_ENABLED", "http-enabled", final Map<String, String> expectedValues = Map.of(
KeycloakDistConfigurator::configureHttp, "true"); "http-enabled", "true",
"http-port", "123",
"https-port", "456",
"https-certificate-file", Constants.CERTIFICATES_FOLDER + "/tls.crt",
"https-certificate-key-file", Constants.CERTIFICATES_FOLDER + "/tls.key"
);
testFirstClassCitizen(expectedValues, KeycloakDistConfigurator::configureHttp);
} }
@Test @Test
public void httpPort() { public void featuresEmptyLists() {
testFirstClassCitizen("KC_HTTP_PORT", "http-port",
KeycloakDistConfigurator::configureHttp, "123");
}
@Test
public void httpsPort() {
testFirstClassCitizen("KC_HTTPS_PORT", "https-port",
KeycloakDistConfigurator::configureHttp, "456");
}
@Test
public void tlsSecret() {
testFirstClassCitizen("KC_HTTPS_CERTIFICATE_FILE", "https-certificate-file",
KeycloakDistConfigurator::configureHttp, Constants.CERTIFICATES_FOLDER + "/tls.crt");
testFirstClassCitizen("KC_HTTPS_CERTIFICATE_KEY_FILE", "https-certificate-key-file",
KeycloakDistConfigurator::configureHttp, Constants.CERTIFICATES_FOLDER + "/tls.key");
}
@Test
public void testEmptyLists() {
final Keycloak keycloak = K8sUtils.getResourceFromFile("test-serialization-keycloak-cr-with-empty-list.yml", Keycloak.class); final Keycloak keycloak = K8sUtils.getResourceFromFile("test-serialization-keycloak-cr-with-empty-list.yml", Keycloak.class);
final StatefulSet deployment = getBasicKcDeployment(); final StatefulSet deployment = getBasicKcDeployment();
final KeycloakDistConfigurator distConfig = new KeycloakDistConfigurator(keycloak, deployment, null); final KeycloakDistConfigurator distConfig = new KeycloakDistConfigurator(keycloak, deployment, null);
@ -103,33 +91,61 @@ public class KeycloakDistConfiguratorTest {
} }
@Test @Test
public void testDatabaseSettings() { public void db() {
testFirstClassCitizen("KC_DB", "db", final Map<String, String> expectedValues = new HashMap<>(Map.of(
KeycloakDistConfigurator::configureDatabase, "vendor"); "db", "vendor",
testFirstClassCitizen("KC_DB_USERNAME", "db-username", "db-username", "usernameSecret",
KeycloakDistConfigurator::configureDatabase, "usernameSecret"); "db-password", "passwordSecret",
testFirstClassCitizen("KC_DB_PASSWORD", "db-password", "db-url-database", "database",
KeycloakDistConfigurator::configureDatabase, "passwordSecret"); "db-schema", "schema",
testFirstClassCitizen("KC_DB_SCHEMA", "db-schema", "db-url-host", "host",
KeycloakDistConfigurator::configureDatabase, "schema"); "db-url-port", "123",
testFirstClassCitizen("KC_DB_URL_HOST", "db-url-host", "db-pool-initial-size", "1",
KeycloakDistConfigurator::configureDatabase, "host"); "db-pool-min-size", "2",
testFirstClassCitizen("KC_DB_URL_PORT", "db-url-port", "db-pool-max-size", "3"
KeycloakDistConfigurator::configureDatabase, "123"); ));
testFirstClassCitizen("KC_DB_POOL_INITIAL_SIZE", "db-pool-initial-size", expectedValues.put("db-url", "url");
KeycloakDistConfigurator::configureDatabase, "1");
testFirstClassCitizen("KC_DB_POOL_MIN_SIZE", "db-pool-min-size", testFirstClassCitizen(expectedValues, KeycloakDistConfigurator::configureDatabase);
KeycloakDistConfigurator::configureDatabase, "2"); }
testFirstClassCitizen("KC_DB_POOL_MAX_SIZE", "db-pool-max-size",
KeycloakDistConfigurator::configureDatabase, "3"); @Test
public void hostname() {
final Map<String, String> expectedValues = Map.of(
"hostname", "my-hostname",
"hostname-admin-url", "https://www.my-admin-hostname.org:8448/something",
"hostname-strict", "true",
"hostname-strict-backchannel", "true",
"hostname-admin", "my-admin-hostname"
);
testFirstClassCitizen(expectedValues, KeycloakDistConfigurator::configureHostname);
}
@Test
public void missingHostname() {
final Keycloak keycloak = K8sUtils.getResourceFromFile("test-serialization-keycloak-cr-with-empty-list.yml", Keycloak.class);
final StatefulSet deployment = getBasicKcDeployment();
final KeycloakDistConfigurator distConfig = new KeycloakDistConfigurator(keycloak, deployment, null);
final List<EnvVar> envVars = deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv();
distConfig.configureHostname();
assertEnvVarNotPresent(envVars, "KC_HOSTNAME");
assertEnvVarNotPresent(envVars, "KC_HOSTNAME_ADMIN");
assertEnvVarNotPresent(envVars, "KC_HOSTNAME_ADMIN_URL");
assertEnvVarNotPresent(envVars, "KC_HOSTNAME_STRICT");
assertEnvVarNotPresent(envVars, "KC_HOSTNAME_STRICT-BACKCHANNEL");
} }
/* UTILS */ /* UTILS */
private void testFirstClassCitizen(String envVarName, String optionName, Consumer<KeycloakDistConfigurator> config, String... expectedValues) {
testFirstClassCitizen("/test-serialization-keycloak-cr.yml", envVarName, optionName, config, expectedValues); private void testFirstClassCitizen(Map<String, String> expectedValues, Consumer<KeycloakDistConfigurator> config) {
testFirstClassCitizen("/test-serialization-keycloak-cr.yml", expectedValues, config);
} }
private void testFirstClassCitizen(String crName, String envVarName, String optionName, Consumer<KeycloakDistConfigurator> config, String... expectedValues) { private void testFirstClassCitizen(String crName, Map<String, String> expectedValues, Consumer<KeycloakDistConfigurator> config) {
final Keycloak keycloak = K8sUtils.getResourceFromFile(crName, Keycloak.class); final Keycloak keycloak = K8sUtils.getResourceFromFile(crName, Keycloak.class);
final StatefulSet deployment = getBasicKcDeployment(); final StatefulSet deployment = getBasicKcDeployment();
final KeycloakDistConfigurator distConfig = new KeycloakDistConfigurator(keycloak, deployment, null); final KeycloakDistConfigurator distConfig = new KeycloakDistConfigurator(keycloak, deployment, null);
@ -137,54 +153,89 @@ public class KeycloakDistConfiguratorTest {
final Container container = deployment.getSpec().getTemplate().getSpec().getContainers().get(0); final Container container = deployment.getSpec().getTemplate().getSpec().getContainers().get(0);
assertThat(container).isNotNull(); assertThat(container).isNotNull();
assertEnvVarNotPresent(container.getEnv(), envVarName); final List<ValueOrSecret> serverConfig = expectedValues.keySet()
assertWarningStatus(distConfig, optionName, false); .stream()
.map(f -> new ValueOrSecret(f, "foo"))
.collect(Collectors.toUnmodifiableList());
keycloak.getSpec().setServerConfiguration(serverConfig);
final var expectedFields = expectedValues.keySet();
assertWarningStatusFirstClassFields(distConfig, false, expectedFields);
expectedValues.forEach((k, v) -> assertEnvVarNotPresent(container.getEnv(), getKeycloakOptionEnvVarName(k)));
config.accept(distConfig); config.accept(distConfig);
assertContainerEnvVar(container.getEnv(), envVarName, expectedValues); assertWarningStatusFirstClassFields(distConfig, true, expectedFields);
expectedValues.forEach((k, v) -> assertContainerEnvVar(container.getEnv(), getKeycloakOptionEnvVarName(k), v));
keycloak.getSpec().setServerConfiguration(List.of(new ValueOrSecret(optionName, "foo")));
assertWarningStatus(distConfig, optionName, true);
} }
/** /**
* assertContainerEnvVar(container.getEnv(), "KC_FEATURES", "admin,authorization"); * assertContainerEnvVar(container.getEnv(), "KC_FEATURES", "admin,authorization");
* assertContainerEnvVar(container.getEnv(), "KC_HOSTNAME", "someHostname"); * assertContainerEnvVar(container.getEnv(), "KC_HOSTNAME", "someHostname");
*/ */
private void assertContainerEnvVar(List<EnvVar> envVars, String varName, String... expectedValue) { private void assertContainerEnvVar(List<EnvVar> envVars, String varName, String expectedValue) {
assertThat(envVars).isNotNull(); assertThat(envVars).isNotNull();
assertEnvVarPresent(envVars, varName); assertEnvVarPresent(envVars, varName);
final List<String> foundValues = getValuesFromEnvVar(envVars, varName); final String foundValue = envVars.stream().filter(f -> varName.equals(f.getName()))
assertThat(CollectionUtil.isNotEmpty(foundValues)).isTrue(); .findFirst()
for (String val : expectedValue) { .map(envVar -> {
assertThat(foundValues.contains(val)).isTrue(); if (envVar.getValue() != null) {
} return envVar.getValue();
}
if (envVar.getValueFrom() != null && envVar.getValueFrom().getSecretKeyRef() != null) {
return envVar.getValueFrom().getSecretKeyRef().getName();
}
return null;
})
.orElse(null);
assertThat(foundValue).isNotNull();
assertThat(foundValue).isEqualTo(expectedValue);
} }
private void assertEnvVarPresent(List<EnvVar> envVars, String varName) { private void assertEnvVarPresent(List<EnvVar> envVars, String varName) {
assertThat(containsEnvironmentVariable(envVars, varName)).isTrue(); assertThat(containsEnvironmentVariable(envVars, varName)).isTrue();
} }
private void assertEnvVarNotPresent(List<EnvVar> envVars, String varName) { private void assertEnvVarNotPresent(List<EnvVar> envVars, String varName) {
assertThat(containsEnvironmentVariable(envVars, varName)).isFalse(); assertThat(containsEnvironmentVariable(envVars, varName)).isFalse();
} }
private void assertWarningStatus(KeycloakDistConfigurator distConfig, String optionName, boolean expectWarning) { private void assertWarningStatusFirstClassFields(KeycloakDistConfigurator distConfig, boolean expectWarning, Collection<String> firstClassFields) {
final String message = "warning: You need to specify these fields as the first-class citizen of the CR: " + optionName; final String message = "warning: You need to specify these fields as the first-class citizen of the CR: ";
final KeycloakStatusBuilder statusBuilder = new KeycloakStatusBuilder(); final KeycloakStatusBuilder statusBuilder = new KeycloakStatusBuilder();
distConfig.validateOptions(statusBuilder); distConfig.validateOptions(statusBuilder);
final KeycloakStatus status = statusBuilder.build(); final KeycloakStatus status = statusBuilder.build();
if (expectWarning) { if (expectWarning) {
assertKeycloakStatusCondition(status, KeycloakStatusCondition.HAS_ERRORS, false, message); assertKeycloakStatusCondition(status, KeycloakStatusCondition.HAS_ERRORS, false, message);
var fullMessage = getFullMessageFromStatus(status, message);
assertThat(fullMessage).isPresent();
var foundFields = fullMessage.get().substring(message.length());
assertThat(isBlank(foundFields)).isFalse();
assertThat(foundFields.split(",")).containsAll(firstClassFields);
} else { } else {
assertKeycloakStatusDoesNotContainMessage(status, message); assertKeycloakStatusDoesNotContainMessage(status, message);
} }
} }
private Optional<String> getFullMessageFromStatus(KeycloakStatus status, String containedMessage) {
if (isBlank(containedMessage)) return Optional.empty();
return status.getConditions()
.stream()
.filter(f -> f.getMessage().contains(containedMessage))
.findAny()
.map(KeycloakStatusCondition::getMessage);
}
private StatefulSet getBasicKcDeployment() { private StatefulSet getBasicKcDeployment() {
return new StatefulSetBuilder() return new StatefulSetBuilder()
.withNewSpec() .withNewSpec()
@ -201,34 +252,7 @@ public class KeycloakDistConfiguratorTest {
} }
private boolean containsEnvironmentVariable(List<EnvVar> envVars, String varName) { private boolean containsEnvironmentVariable(List<EnvVar> envVars, String varName) {
if (CollectionUtil.isEmpty(envVars) || ObjectUtil.isBlank(varName)) return false; if (CollectionUtil.isEmpty(envVars) || isBlank(varName)) return false;
return envVars.stream().anyMatch(f -> varName.equals(f.getName())); return envVars.stream().anyMatch(f -> varName.equals(f.getName()));
} }
}
/**
* Returns values of environment variable separated by comma (f.e KC_FEATURES=admin2,ciba)
*/
private List<String> getValuesFromEnvVar(List<EnvVar> envVars, String varName) {
if (CollectionUtil.isEmpty(envVars) || ObjectUtil.isBlank(varName)) return Collections.emptyList();
return envVars.stream().filter(f -> varName.equals(f.getName()))
.findFirst()
.map(new Function<EnvVar, String>() {
@Override
public String apply(EnvVar envVar) {
if (envVar.getValue() != null) {
return envVar.getValue();
}
if (envVar.getValueFrom() != null && envVar.getValueFrom().getSecretKeyRef() != null) {
return envVar.getValueFrom().getSecretKeyRef().getName();
}
return null;
}
})
.map(f -> f.split(","))
.map(List::of)
.orElseGet(Collections::emptyList);
}
}

View file

@ -28,8 +28,9 @@ import org.junit.jupiter.api.Test;
import org.keycloak.operator.Config; import org.keycloak.operator.Config;
import org.keycloak.operator.controllers.KeycloakDeployment; import org.keycloak.operator.controllers.KeycloakDeployment;
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakSpec; import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakSpecBuilder;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.HttpSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.HostnameSpecBuilder;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.HttpSpecBuilder;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.UnsupportedSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.UnsupportedSpec;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -56,15 +57,15 @@ public class PodTemplateTest {
} }
}; };
var kc = new Keycloak(); var kc = new Keycloak();
var spec = new KeycloakSpec();
spec.setUnsupported(new UnsupportedSpec(podTemplate));
spec.setHostname("example.com");
var httpSpec = new HttpSpec(); var httpSpec = new HttpSpecBuilder().withTlsSecret("example-tls-secret").build();
httpSpec.setTlsSecret("example-tls-secret"); var hostnameSpec = new HostnameSpecBuilder().withHostname("example.com").build();
spec.setHttpSpec(httpSpec);
kc.setSpec(new KeycloakSpecBuilder().withUnsupported(new UnsupportedSpec(podTemplate))
.withHttpSpec(httpSpec)
.withHostnameSpec(hostnameSpec)
.build());
kc.setSpec(spec);
var deployment = new KeycloakDeployment(null, config, kc, existingDeployment, "dummy-admin"); var deployment = new KeycloakDeployment(null, config, kc, existingDeployment, "dummy-admin");
return (StatefulSet) deployment.getReconciledResource().get(); return (StatefulSet) deployment.getReconciledResource().get();
} }

View file

@ -13,7 +13,8 @@ spec:
value: postgres value: postgres
- name: db-password - name: db-password
value: testpassword value: testpassword
hostname: example.com hostname:
hostname: example.com
unsupported: unsupported:
podTemplate: podTemplate:
metadata: metadata:

View file

@ -13,6 +13,7 @@ spec:
value: postgres value: postgres
- name: db-password - name: db-password
value: testpassword value: testpassword
hostname: example.com hostname:
hostname: example.com
unsupported: unsupported:
podTemplate: podTemplate:

View file

@ -6,6 +6,5 @@ spec:
features: features:
enabled: enabled:
- -
hostname: my-hostname
http: http:
tlsSecret: my-tls-secret tlsSecret: my-tls-secret

View file

@ -10,7 +10,6 @@ spec:
value: value1 value: value1
- name: features - name: features
value: docker value: docker
hostname: my-hostname
db: db:
vendor: vendor vendor: vendor
usernameSecret: usernameSecret:
@ -34,6 +33,12 @@ spec:
httpPort: 123 httpPort: 123
httpsPort: 456 httpsPort: 456
tlsSecret: my-tls-secret tlsSecret: my-tls-secret
hostname:
hostname: my-hostname
admin: my-admin-hostname
adminUrl: https://www.my-admin-hostname.org:8448/something
strict: true
strictBackchannel: true
features: features:
enabled: enabled:
- docker - docker