diff --git a/operator/src/main/java/org/keycloak/operator/Constants.java b/operator/src/main/java/org/keycloak/operator/Constants.java index 080e37055a..3de7e1fb30 100644 --- a/operator/src/main/java/org/keycloak/operator/Constants.java +++ b/operator/src/main/java/org/keycloak/operator/Constants.java @@ -42,7 +42,8 @@ public final class Constants { public static final Map DEFAULT_DIST_CONFIG = Map.of( "health-enabled","true", "cache", "ispn", - "cache-stack", "kubernetes" + "cache-stack", "kubernetes", + "proxy", "passthrough" ); public static final Integer KEYCLOAK_HTTP_PORT = 8080; diff --git a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeployment.java b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeployment.java index f210ae8b68..ae37b15800 100644 --- a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeployment.java +++ b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeployment.java @@ -20,6 +20,7 @@ import io.fabric8.kubernetes.api.model.Container; import io.fabric8.kubernetes.api.model.EnvVar; import io.fabric8.kubernetes.api.model.EnvVarBuilder; import io.fabric8.kubernetes.api.model.EnvVarSourceBuilder; +import io.fabric8.kubernetes.api.model.ExecActionBuilder; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.PodTemplateSpec; import io.fabric8.kubernetes.api.model.ResourceRequirements; @@ -34,7 +35,9 @@ import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusBuilder; import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Base64; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -44,7 +47,7 @@ import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; -import static io.smallrye.config.common.utils.StringUtil.replaceNonAlphanumericByUnderscores; +import static org.keycloak.operator.crds.v2alpha1.CRDUtils.isTlsConfigured; public class KeycloakDeployment extends OperatorManagedResource implements StatusUpdater { @@ -392,6 +395,31 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu container.setEnv(getEnvVars()); + // probes + var tlsConfigured = isTlsConfigured(keycloakCR); + var userRelativePath = readConfigurationValue(Constants.KEYCLOAK_HTTP_RELATIVE_PATH_KEY); + var kcRelativePath = (userRelativePath == null) ? "" : userRelativePath; + var protocol = !tlsConfigured ? "http" : "https"; + var kcPort = KeycloakService.getServicePort(keycloakCR); + + var baseProbe = new ArrayList<>(List.of("curl", "--head", "--fail", "--silent")); + + if (tlsConfigured) { + baseProbe.add("--insecure"); + } + + var readyProbe = new ArrayList<>(baseProbe); + readyProbe.add(protocol + "://127.0.0.1:" + kcPort + kcRelativePath + "/health/ready"); + var liveProbe = new ArrayList<>(baseProbe); + liveProbe.add(protocol + "://127.0.0.1:" + kcPort + kcRelativePath + "/health/live"); + + container + .getReadinessProbe() + .setExec(new ExecActionBuilder().withCommand(readyProbe).build()); + container + .getLivenessProbe() + .setExec(new ExecActionBuilder().withCommand(liveProbe).build()); + return baseDeployment; } @@ -417,7 +445,7 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu serverConfigSecretsNames = new HashSet<>(); List envVars = serverConfig.stream() .map(v -> { - var envBuilder = new EnvVarBuilder().withName(getEnvVarName(v.getName())); + var envBuilder = new EnvVarBuilder().withName(KeycloakDistConfigurator.getKeycloakOptionEnvVarName(v.getName())); var secret = v.getSecret(); if (secret != null) { envBuilder.withValueFrom( @@ -490,8 +518,8 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu public Set getConfigSecretsNames() { Set ret = new HashSet<>(serverConfigSecretsNames); - if (!keycloakCR.getSpec().isHttp()) { - ret.add(keycloakCR.getSpec().getTlsSecret()); + if (isTlsConfigured(keycloakCR)) { + ret.add(keycloakCR.getSpec().getHttpSpec().getTlsSecret()); } return ret; } @@ -535,8 +563,41 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu } } - public static String getEnvVarName(String kcConfigName) { - // TODO make this use impl from Quarkus dist (Configuration.toEnvVarFormat) - return "KC_" + replaceNonAlphanumericByUnderscores(kcConfigName).toUpperCase(); + protected String readConfigurationValue(String key) { + if (keycloakCR != null && + keycloakCR.getSpec() != null && + keycloakCR.getSpec().getServerConfiguration() != null + ) { + + var serverConfigValue = keycloakCR + .getSpec() + .getServerConfiguration() + .stream() + .filter(sc -> sc.getName().equals(key)) + .findFirst(); + if (serverConfigValue.isPresent()) { + if (serverConfigValue.get().getValue() != null) { + return serverConfigValue.get().getValue(); + } else { + var secretSelector = serverConfigValue.get().getSecret(); + if (secretSelector == null) { + throw new IllegalStateException("Secret " + serverConfigValue.get().getName() + " not defined"); + } + var secret = client.secrets().inNamespace(keycloakCR.getMetadata().getNamespace()).withName(secretSelector.getName()).get(); + if (secret == null) { + throw new IllegalStateException("Secret " + secretSelector.getName() + " not found in cluster"); + } + if (secret.getData().containsKey(secretSelector.getKey())) { + return new String(Base64.getDecoder().decode(secret.getData().get(secretSelector.getKey())), StandardCharsets.UTF_8); + } else { + throw new IllegalStateException("Secret " + secretSelector.getName() + " doesn't contain the expected key " + secretSelector.getKey()); + } + } + } else { + return null; + } + } else { + return null; + } } } diff --git a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDistConfigurator.java b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDistConfigurator.java index 2ada21d9c0..cd43057ffd 100644 --- a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDistConfigurator.java +++ b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDistConfigurator.java @@ -19,7 +19,6 @@ package org.keycloak.operator.controllers; import io.fabric8.kubernetes.api.model.EnvVar; import io.fabric8.kubernetes.api.model.EnvVarBuilder; -import io.fabric8.kubernetes.api.model.ExecActionBuilder; import io.fabric8.kubernetes.api.model.VolumeBuilder; import io.fabric8.kubernetes.api.model.VolumeMountBuilder; import io.fabric8.kubernetes.api.model.apps.StatefulSet; @@ -31,11 +30,10 @@ import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusBuilder; import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret; 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.TransactionsSpec; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Base64; import java.util.Collection; import java.util.HashSet; import java.util.List; @@ -44,7 +42,8 @@ import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; -import static org.keycloak.operator.controllers.KeycloakDeployment.getEnvVarName; +import static io.smallrye.config.common.utils.StringUtil.replaceNonAlphanumericByUnderscores; +import static org.keycloak.operator.crds.v2alpha1.CRDUtils.isTlsConfigured; /** * Configuration for the KeycloakDeployment @@ -70,9 +69,9 @@ public class KeycloakDistConfigurator { */ public void configureDistOptions() { configureHostname(); - configureTLS(); configureFeatures(); configureTransactions(); + configureHttp(); } /** @@ -113,85 +112,6 @@ public class KeycloakDistConfigurator { } } - public void configureTLS() { - var kcContainer = deployment.getSpec().getTemplate().getSpec().getContainers().get(0); - var tlsSecret = keycloakCR.getSpec().getTlsSecret(); - var envVars = kcContainer.getEnv(); - - if (keycloakCR.getSpec().isHttp()) { - var disableTls = List.of( - new EnvVarBuilder() - .withName("KC_HTTP_ENABLED") - .withValue("true") - .build(), - new EnvVarBuilder() - .withName("KC_HOSTNAME_STRICT_HTTPS") - .withValue("false") - .build(), - new EnvVarBuilder() - .withName("KC_PROXY") - .withValue("edge") - .build()); - - envVars.addAll(disableTls); - } else { - var enabledTls = List.of( - new EnvVarBuilder() - .withName("KC_HTTPS_CERTIFICATE_FILE") - .withValue(Constants.CERTIFICATES_FOLDER + "/tls.crt") - .build(), - new EnvVarBuilder() - .withName("KC_HTTPS_CERTIFICATE_KEY_FILE") - .withValue(Constants.CERTIFICATES_FOLDER + "/tls.key") - .build(), - new EnvVarBuilder() - .withName("KC_PROXY") - .withValue("passthrough") - .build()); - - envVars.addAll(enabledTls); - - var volume = new VolumeBuilder() - .withName("keycloak-tls-certificates") - .withNewSecret() - .withSecretName(tlsSecret) - .withOptional(false) - .endSecret() - .build(); - - var volumeMount = new VolumeMountBuilder() - .withName(volume.getName()) - .withMountPath(Constants.CERTIFICATES_FOLDER) - .build(); - - deployment.getSpec().getTemplate().getSpec().getVolumes().add(volume); - kcContainer.getVolumeMounts().add(volumeMount); - } - - var userRelativePath = readConfigurationValue(Constants.KEYCLOAK_HTTP_RELATIVE_PATH_KEY); - var kcRelativePath = (userRelativePath == null) ? "" : userRelativePath; - var protocol = (keycloakCR.getSpec().isHttp()) ? "http" : "https"; - var kcPort = (keycloakCR.getSpec().isHttp()) ? Constants.KEYCLOAK_HTTP_PORT : Constants.KEYCLOAK_HTTPS_PORT; - - var baseProbe = new ArrayList<>(List.of("curl", "--head", "--fail", "--silent")); - - if (!keycloakCR.getSpec().isHttp()) { - baseProbe.add("--insecure"); - } - - var readyProbe = new ArrayList<>(baseProbe); - readyProbe.add(protocol + "://127.0.0.1:" + kcPort + kcRelativePath + "/health/ready"); - var liveProbe = new ArrayList<>(baseProbe); - liveProbe.add(protocol + "://127.0.0.1:" + kcPort + kcRelativePath + "/health/live"); - - kcContainer - .getReadinessProbe() - .setExec(new ExecActionBuilder().withCommand(readyProbe).build()); - kcContainer - .getLivenessProbe() - .setExec(new ExecActionBuilder().withCommand(liveProbe).build()); - } - public void configureFeatures() { optionMapper(keycloakCR.getSpec().getFeatureSpec()) .mapOptionFromCollection("features", FeatureSpec::getEnabledFeatures) @@ -203,46 +123,50 @@ public class KeycloakDistConfigurator { .mapOption("transaction-xa-enabled", TransactionsSpec::isXaEnabled); } - /* ---------- END of configuration of first-class citizen fields ---------- */ + public void configureHttp() { + var optionMapper = optionMapper(keycloakCR.getSpec().getHttpSpec()) + .mapOption("http-enabled", HttpSpec::getHttpEnabled) + .mapOption("http-port", HttpSpec::getHttpPort) + .mapOption("https-port", HttpSpec::getHttpsPort); - protected String readConfigurationValue(String key) { - if (keycloakCR != null && - keycloakCR.getSpec() != null && - keycloakCR.getSpec().getServerConfiguration() != null - ) { - - var serverConfigValue = keycloakCR - .getSpec() - .getServerConfiguration() - .stream() - .filter(sc -> sc.getName().equals(key)) - .findFirst(); - if (serverConfigValue.isPresent()) { - if (serverConfigValue.get().getValue() != null) { - return serverConfigValue.get().getValue(); - } else { - var secretSelector = serverConfigValue.get().getSecret(); - if (secretSelector == null) { - throw new IllegalStateException("Secret " + serverConfigValue.get().getName() + " not defined"); - } - var secret = client.secrets().inNamespace(keycloakCR.getMetadata().getNamespace()).withName(secretSelector.getName()).get(); - if (secret == null) { - throw new IllegalStateException("Secret " + secretSelector.getName() + " not found in cluster"); - } - if (secret.getData().containsKey(secretSelector.getKey())) { - return new String(Base64.getDecoder().decode(secret.getData().get(secretSelector.getKey())), StandardCharsets.UTF_8); - } else { - throw new IllegalStateException("Secret " + secretSelector.getName() + " doesn't contain the expected key " + secretSelector.getKey()); - } - } - } else { - return null; - } - } else { - return null; - } + configureTLS(optionMapper); } + public void configureTLS(OptionMapper optionMapper) { + final String certFileOptionName = "https-certificate-file"; + final String keyFileOptionName = "https-certificate-key-file"; + + if (!isTlsConfigured(keycloakCR)) { + // for mapping and triggering warning in status if someone uses the fields directly + optionMapper.mapOption(certFileOptionName); + optionMapper.mapOption(keyFileOptionName); + return; + } + + optionMapper.mapOption(certFileOptionName, Constants.CERTIFICATES_FOLDER + "/tls.crt"); + optionMapper.mapOption(keyFileOptionName, Constants.CERTIFICATES_FOLDER + "/tls.key"); + + var kcContainer = deployment.getSpec().getTemplate().getSpec().getContainers().get(0); + + var volume = new VolumeBuilder() + .withName("keycloak-tls-certificates") + .withNewSecret() + .withSecretName(keycloakCR.getSpec().getHttpSpec().getTlsSecret()) + .withOptional(false) + .endSecret() + .build(); + + var volumeMount = new VolumeMountBuilder() + .withName(volume.getName()) + .withMountPath(Constants.CERTIFICATES_FOLDER) + .build(); + + deployment.getSpec().getTemplate().getSpec().getVolumes().add(volume); + kcContainer.getVolumeMounts().add(volumeMount); + } + + /* ---------- END of configuration of first-class citizen fields ---------- */ + /** * Assume the specified first-class citizens are not included in the general server configuration * @@ -263,6 +187,11 @@ public class KeycloakDistConfigurator { } } + public static String getKeycloakOptionEnvVarName(String kcConfigName) { + // TODO make this use impl from Quarkus dist (Configuration.toEnvVarFormat) + return "KC_" + replaceNonAlphanumericByUnderscores(kcConfigName).toUpperCase(); + } + private OptionMapper optionMapper(T optionSpec) { return new OptionMapper<>(optionSpec); } @@ -300,7 +229,7 @@ public class KeycloakDistConfigurator { } EnvVar envVar = new EnvVarBuilder() - .withName(getEnvVarName(optionName)) + .withName(getKeycloakOptionEnvVarName(optionName)) .withValue(valueStr) .build(); @@ -309,6 +238,10 @@ public class KeycloakDistConfigurator { return this; } + public OptionMapper mapOption(String optionName) { + return mapOption(optionName, s -> null); + } + public OptionMapper mapOption(String optionName, R optionValue) { return mapOption(optionName, s -> optionValue); } 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 a96f9349d8..0f86844330 100644 --- a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakIngress.java +++ b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakIngress.java @@ -27,6 +27,8 @@ import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusBuilder; import java.util.HashMap; import java.util.Optional; +import static org.keycloak.operator.crds.v2alpha1.CRDUtils.isTlsConfigured; + public class KeycloakIngress extends OperatorManagedResource implements StatusUpdater { private final Ingress existingIngress; @@ -59,8 +61,8 @@ public class KeycloakIngress extends OperatorManagedResource implements StatusUp } private Ingress newIngress() { - var port = (keycloak.getSpec().isHttp()) ? Constants.KEYCLOAK_HTTP_PORT : Constants.KEYCLOAK_HTTPS_PORT; - var backendProtocol = (keycloak.getSpec().isHttp()) ? "HTTP" : "HTTPS"; + var port = KeycloakService.getServicePort(keycloak); + var backendProtocol = (!isTlsConfigured(keycloak)) ? "HTTP" : "HTTPS"; Ingress ingress = new IngressBuilder() .withNewMetadata() diff --git a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakRealmImportJob.java b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakRealmImportJob.java index 28465e3a29..776df772b0 100644 --- a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakRealmImportJob.java +++ b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakRealmImportJob.java @@ -38,8 +38,7 @@ import org.keycloak.operator.crds.v2alpha1.realmimport.KeycloakRealmImportStatus import java.util.List; import java.util.Optional; -import static org.keycloak.operator.Constants.DEFAULT_DIST_CONFIG; -import static org.keycloak.operator.controllers.KeycloakDeployment.getEnvVarName; +import static org.keycloak.operator.controllers.KeycloakDistConfigurator.getKeycloakOptionEnvVarName; public class KeycloakRealmImportJob extends OperatorManagedResource { @@ -144,8 +143,8 @@ public class KeycloakRealmImportJob extends OperatorManagedResource { .get(0) .getEnv(); - var cacheEnvVarName = getEnvVarName("cache"); - var healthEnvVarName = getEnvVarName("health-enabled"); + var cacheEnvVarName = getKeycloakOptionEnvVarName("cache"); + var healthEnvVarName = getKeycloakOptionEnvVarName("health-enabled"); envvars.removeIf(e -> e.getName().equals(cacheEnvVarName) || e.getName().equals(healthEnvVarName)); // The Job should not connect to the cache diff --git a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakService.java b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakService.java index ef900e91b9..3cf24e6b5f 100644 --- a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakService.java +++ b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakService.java @@ -25,9 +25,13 @@ import io.fabric8.kubernetes.client.KubernetesClient; import org.keycloak.operator.Constants; import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusBuilder; +import org.keycloak.operator.crds.v2alpha1.deployment.spec.HttpSpec; import java.util.Optional; +import static org.keycloak.operator.crds.v2alpha1.CRDUtils.getValueFromSubSpec; +import static org.keycloak.operator.crds.v2alpha1.CRDUtils.isTlsConfigured; + public class KeycloakService extends OperatorManagedResource implements StatusUpdater { private Service existingService; @@ -40,10 +44,9 @@ public class KeycloakService extends OperatorManagedResource implements StatusUp } private ServiceSpec getServiceSpec() { - var port = (this.keycloak.getSpec().isHttp()) ? Constants.KEYCLOAK_HTTP_PORT : Constants.KEYCLOAK_HTTPS_PORT; - return new ServiceSpecBuilder() + return new ServiceSpecBuilder() .addNewPort() - .withPort(port) + .withPort(getServicePort(keycloak)) .withProtocol(Constants.KEYCLOAK_SERVICE_PROTOCOL) .endPort() .withSelector(Constants.DEFAULT_LABELS) @@ -91,4 +94,13 @@ public class KeycloakService extends OperatorManagedResource implements StatusUp public String getName() { return cr.getMetadata().getName() + Constants.KEYCLOAK_SERVICE_SUFFIX; } + + public static int getServicePort(Keycloak keycloak) { + // we assume HTTP when TLS is not configureed + if (!isTlsConfigured(keycloak)) { + return getValueFromSubSpec(keycloak.getSpec().getHttpSpec(), HttpSpec::getHttpPort).orElse(Constants.KEYCLOAK_HTTP_PORT); + } else { + return getValueFromSubSpec(keycloak.getSpec().getHttpSpec(), HttpSpec::getHttpsPort).orElse(Constants.KEYCLOAK_HTTPS_PORT); + } + } } diff --git a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/CRDUtils.java b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/CRDUtils.java new file mode 100644 index 0000000000..c3333309b3 --- /dev/null +++ b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/CRDUtils.java @@ -0,0 +1,42 @@ +/* + * 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; + +import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; +import org.keycloak.operator.crds.v2alpha1.deployment.spec.HttpSpec; + +import java.util.Optional; +import java.util.function.Function; + +/** + * @author Vaclav Muzikar + */ +public final class CRDUtils { + public static boolean isTlsConfigured(Keycloak keycloakCR) { + var tlsSecret = getValueFromSubSpec(keycloakCR.getSpec().getHttpSpec(), HttpSpec::getTlsSecret); + return tlsSecret.isPresent() && !tlsSecret.get().trim().isEmpty(); + } + + public static Optional getValueFromSubSpec(T subSpec, Function valueSupplier) { + if (subSpec != null) { + return Optional.ofNullable(valueSupplier.apply(subSpec)); + } else { + return Optional.empty(); + } + } +} diff --git a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakSpec.java b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakSpec.java index e5b1de3cde..cdf36962cf 100644 --- a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakSpec.java +++ b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakSpec.java @@ -23,6 +23,8 @@ import com.fasterxml.jackson.annotation.JsonPropertyDescription; import io.fabric8.kubernetes.api.model.LocalObjectReference; import org.keycloak.operator.Constants; 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.UnsupportedSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.TransactionsSpec; import javax.validation.constraints.NotNull; @@ -43,18 +45,14 @@ 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.") private List serverConfiguration; // can't use Set due to a bug in Sundrio https://github.com/sundrio/sundrio/issues/316 - // TODO: switch to this serverConfig when all the options are ported - // private ServerConfig serverConfig; - @NotNull @JsonPropertyDescription("Hostname for the Keycloak server.\n" + "The special value `" + Constants.INSECURE_DISABLE + "` disables the hostname strict resolution.") private String hostname; - @NotNull - @JsonPropertyDescription("A secret containing the TLS configuration for HTTPS. Reference: https://kubernetes.io/docs/concepts/configuration/secret/#tls-secrets.\n" + - "The special value `" + Constants.INSECURE_DISABLE + "` disables https.") - private String tlsSecret; + @JsonProperty("http") + @JsonPropertyDescription("In this section you can configure Keycloak features related to HTTP and HTTPS") + private HttpSpec httpSpec; @JsonPropertyDescription("Disable the default ingress.") private boolean disableDefaultIngress; @@ -62,7 +60,7 @@ public class KeycloakSpec { @JsonPropertyDescription( "In this section you can configure podTemplate advanced features, not production-ready, and not supported settings.\n" + "Use at your own risk and open an issue with your use-case if you don't find an alternative way.") - private KeycloakSpecUnsupported unsupported; + private UnsupportedSpec unsupported; @JsonProperty("features") @JsonPropertyDescription("In this section you can configure Keycloak features, which should be enabled/disabled.") @@ -85,6 +83,14 @@ public class KeycloakSpec { return this.hostname.equals(Constants.INSECURE_DISABLE); } + public HttpSpec getHttpSpec() { + return httpSpec; + } + + public void setHttpSpec(HttpSpec httpSpec) { + this.httpSpec = httpSpec; + } + public void setDisableDefaultIngress(boolean value) { this.disableDefaultIngress = value; } @@ -93,24 +99,11 @@ public class KeycloakSpec { return this.disableDefaultIngress; } - public String getTlsSecret() { - return tlsSecret; - } - - public void setTlsSecret(String tlsSecret) { - this.tlsSecret = tlsSecret; - } - - @JsonIgnore - public boolean isHttp() { - return this.tlsSecret.equals(Constants.INSECURE_DISABLE); - } - - public KeycloakSpecUnsupported getUnsupported() { + public UnsupportedSpec getUnsupported() { return unsupported; } - public void setUnsupported(KeycloakSpecUnsupported unsupported) { + public void setUnsupported(UnsupportedSpec unsupported) { this.unsupported = unsupported; } diff --git a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakStatusCondition.java b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakStatusCondition.java index 6ad95092e2..32dd23fdce 100644 --- a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakStatusCondition.java +++ b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakStatusCondition.java @@ -68,4 +68,13 @@ public class KeycloakStatusCondition { public int hashCode() { return Objects.hash(getType(), getStatus(), getMessage()); } + + @Override + public String toString() { + return "KeycloakStatusCondition{" + + "type='" + type + '\'' + + ", status=" + status + + ", message='" + message + '\'' + + '}'; + } } 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 new file mode 100644 index 0000000000..fe31fba316 --- /dev/null +++ b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/HttpSpec.java @@ -0,0 +1,72 @@ +/* + * 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 org.keycloak.operator.Constants; + +/** + * @author Vaclav Muzikar + */ +@Buildable(editableEnabled = false, builderPackage = "io.fabric8.kubernetes.api.builder") +public class HttpSpec { + @JsonPropertyDescription("A secret containing the TLS configuration for HTTPS. Reference: https://kubernetes.io/docs/concepts/configuration/secret/#tls-secrets.") + private String tlsSecret; + + @JsonPropertyDescription("Enables the HTTP listener.") + private Boolean httpEnabled; + + @JsonPropertyDescription("The used HTTP port.") + private Integer httpPort = Constants.KEYCLOAK_HTTP_PORT; + + @JsonPropertyDescription("The used HTTPS port.") + private Integer httpsPort = Constants.KEYCLOAK_HTTPS_PORT; + + public String getTlsSecret() { + return tlsSecret; + } + + public void setTlsSecret(String tlsSecret) { + this.tlsSecret = tlsSecret; + } + + public Boolean getHttpEnabled() { + return httpEnabled; + } + + public void setHttpEnabled(Boolean httpEnabled) { + this.httpEnabled = httpEnabled; + } + + public Integer getHttpPort() { + return httpPort; + } + + public void setHttpPort(Integer httpPort) { + this.httpPort = httpPort; + } + + public Integer getHttpsPort() { + return httpsPort; + } + + public void setHttpsPort(Integer httpsPort) { + this.httpsPort = httpsPort; + } +} diff --git a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakSpecUnsupported.java b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/UnsupportedSpec.java similarity index 57% rename from operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakSpecUnsupported.java rename to operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/UnsupportedSpec.java index 54522c7e63..1a42a7ea8d 100644 --- a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakSpecUnsupported.java +++ b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/UnsupportedSpec.java @@ -1,4 +1,21 @@ -package org.keycloak.operator.crds.v2alpha1.deployment; +/* + * 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.fabric8.kubernetes.api.model.PodTemplateSpec; @@ -10,16 +27,16 @@ import io.sundr.builder.annotations.BuildableReference; @BuildableReference(io.fabric8.kubernetes.api.model.ObjectMeta.class), @BuildableReference(io.fabric8.kubernetes.api.model.PodTemplateSpec.class) }) -public class KeycloakSpecUnsupported { +public class UnsupportedSpec { @JsonPropertyDescription("You can configure that will be merged with the one configured by default by the operator.\n" + "Use at your own risk, we reserve the possibility to remove/change the way any field gets merged in future releases without notice.\n" + "Reference: https://kubernetes.io/docs/concepts/workloads/pods/#pod-templates") private PodTemplateSpec podTemplate; - public KeycloakSpecUnsupported() {} + public UnsupportedSpec() {} - public KeycloakSpecUnsupported(PodTemplateSpec podTemplate) { + public UnsupportedSpec(PodTemplateSpec podTemplate) { this.podTemplate = podTemplate; } diff --git a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/realmimport/KeycloakRealmImportStatusCondition.java b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/realmimport/KeycloakRealmImportStatusCondition.java index 9000c6036e..f3a11b1e19 100644 --- a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/realmimport/KeycloakRealmImportStatusCondition.java +++ b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/realmimport/KeycloakRealmImportStatusCondition.java @@ -65,4 +65,13 @@ public class KeycloakRealmImportStatusCondition { public int hashCode() { return Objects.hash(getType(), getStatus(), getMessage()); } + + @Override + public String toString() { + return "KeycloakRealmImportStatusCondition{" + + "type='" + type + '\'' + + ", status=" + status + + ", message='" + message + '\'' + + '}'; + } } diff --git a/operator/src/main/resources/example-keycloak.yaml b/operator/src/main/resources/example-keycloak.yaml index d0628b27c6..cafd8555ef 100644 --- a/operator/src/main/resources/example-keycloak.yaml +++ b/operator/src/main/resources/example-keycloak.yaml @@ -18,4 +18,5 @@ spec: name: keycloak-db-secret key: password hostname: example.com - tlsSecret: example-tls-secret \ No newline at end of file + http: + tlsSecret: example-tls-secret \ No newline at end of file diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakDeploymentTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakDeploymentTest.java index 00ed76f035..a0bf8c003d 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakDeploymentTest.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakDeploymentTest.java @@ -30,10 +30,10 @@ import org.awaitility.Awaitility; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfSystemProperty; 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.KeycloakDeployment; import org.keycloak.operator.controllers.KeycloakService; import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret; @@ -110,7 +110,7 @@ public class KeycloakDeploymentTest extends BaseOperatorTest { .getSpec().getTemplate().getSpec().getContainers().get(0); assertThat(c.getImage()).isEqualTo("quay.io/keycloak/non-existing-keycloak"); assertThat(c.getEnv().stream() - .anyMatch(e -> e.getName().equals(KeycloakDeployment.getEnvVarName(dbConf.getName())) + .anyMatch(e -> e.getName().equals(KeycloakDistConfigurator.getKeycloakOptionEnvVarName(dbConf.getName())) && e.getValue().equals(dbConf.getValue()))) .isTrue(); }); @@ -127,7 +127,7 @@ public class KeycloakDeploymentTest extends BaseOperatorTest { var kc = getDefaultKeycloakDeployment(); var health = new ValueOrSecret("health-enabled", "false"); var e = new EnvVarBuilder() - .withName(KeycloakDeployment.getEnvVarName(health.getName())) + .withName(KeycloakDistConfigurator.getKeycloakOptionEnvVarName(health.getName())) .withValue(health.getValue()) .build(); kc.getSpec().getServerConfiguration().add(health); @@ -218,21 +218,11 @@ public class KeycloakDeploymentTest extends BaseOperatorTest { public void testTlsDisabled() { try { var kc = getDefaultKeycloakDeployment(); - kc.getSpec().setTlsSecret(Constants.INSECURE_DISABLE); + kc.getSpec().getHttpSpec().setTlsSecret(null); + kc.getSpec().getHttpSpec().setHttpEnabled(true); deployKeycloak(k8sclient, kc, true); - var service = new KeycloakService(k8sclient, kc); - Awaitility.await() - .ignoreExceptions() - .untilAsserted(() -> { - String url = "http://" + service.getName() + "." + namespace + ":" + Constants.KEYCLOAK_HTTP_PORT; - Log.info("Checking url: " + url); - - var curlOutput = K8sUtils.inClusterCurl(k8sclient, namespace, url); - Log.info("Curl Output: " + curlOutput); - - assertEquals("200", curlOutput); - }); + assertKeycloakAccessibleViaService(kc, false, Constants.KEYCLOAK_HTTP_PORT); } catch (Exception e) { savePodLogs(); throw e; @@ -288,6 +278,44 @@ public class KeycloakDeploymentTest extends BaseOperatorTest { } } + @Test + public void testHttpsPort() { + try { + final int httpsPort = 8543; + final int httpPort = 8180; + var kc = getDefaultKeycloakDeployment(); + kc.getSpec().setHostname(Constants.INSECURE_DISABLE); + kc.getSpec().getHttpSpec().setHttpsPort(httpsPort); + kc.getSpec().getHttpSpec().setHttpPort(httpPort); + deployKeycloak(k8sclient, kc, true); + + assertKeycloakAccessibleViaService(kc, true, httpsPort); + } catch (Exception e) { + savePodLogs(); + throw e; + } + } + + @Test + public void testHttpPort() { + try { + final int httpsPort = 8543; + final int httpPort = 8180; + var kc = getDefaultKeycloakDeployment(); + kc.getSpec().setHostname(Constants.INSECURE_DISABLE); + kc.getSpec().getHttpSpec().setHttpsPort(httpsPort); + kc.getSpec().getHttpSpec().setHttpPort(httpPort); + kc.getSpec().getHttpSpec().setTlsSecret(null); + kc.getSpec().getHttpSpec().setHttpEnabled(true); + deployKeycloak(k8sclient, kc, true); + + assertKeycloakAccessibleViaService(kc, false, httpPort); + } catch (Exception e) { + savePodLogs(); + throw e; + } + } + // Reference curl command: // curl --insecure --data "grant_type=password&client_id=admin-cli&username=admin&password=adminPassword" https://localhost:8443/realms/master/protocol/openid-connect/token @Test @@ -542,4 +570,20 @@ public class KeycloakDeploymentTest extends BaseOperatorTest { LocalObjectReference localObjRefAsSecretTmp = new LocalObjectReferenceBuilder().withName(imagePullSecret.getMetadata().getName()).build(); keycloakCR.getSpec().setImagePullSecrets(Collections.singletonList(localObjRefAsSecretTmp)); } + + private void assertKeycloakAccessibleViaService(Keycloak kc, boolean https, int port) { + var service = new KeycloakService(k8sclient, kc); + Awaitility.await() + .ignoreExceptions() + .untilAsserted(() -> { + String protocol = https ? "https" : "http"; + String url = protocol + "://" + service.getName() + "." + namespace + ":" + port; + Log.info("Checking url: " + url); + + var curlOutput = K8sUtils.inClusterCurl(k8sclient, namespace, url); + Log.info("Curl Output: " + curlOutput); + + assertEquals("200", curlOutput); + }); + } } 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 5f88a473c9..03533c7140 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,7 +39,8 @@ public class KeycloakIngressTest extends BaseOperatorTest { public void testIngressOnHTTP() { var kc = K8sUtils.getDefaultKeycloakDeployment(); kc.getSpec().setHostname(Constants.INSECURE_DISABLE); - kc.getSpec().setTlsSecret(Constants.INSECURE_DISABLE); + kc.getSpec().getHttpSpec().setTlsSecret(null); + kc.getSpec().getHttpSpec().setHttpEnabled(true); K8sUtils.deployKeycloak(k8sclient, kc, true); Awaitility.await() diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/integration/RealmImportTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/integration/RealmImportTest.java index 0620676b53..2efb04df37 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/integration/RealmImportTest.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/integration/RealmImportTest.java @@ -28,7 +28,7 @@ import org.junit.jupiter.api.condition.EnabledIfSystemProperty; import org.keycloak.operator.testsuite.utils.CRAssert; import org.keycloak.operator.controllers.KeycloakService; import org.keycloak.operator.crds.v2alpha1.realmimport.KeycloakRealmImport; -import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakSpecUnsupported; +import org.keycloak.operator.crds.v2alpha1.deployment.spec.UnsupportedSpec; import java.util.stream.Collectors; @@ -36,7 +36,7 @@ import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.SECONDS; import static org.assertj.core.api.Assertions.assertThat; import static org.keycloak.operator.Constants.KEYCLOAK_HTTPS_PORT; -import static org.keycloak.operator.controllers.KeycloakDeployment.getEnvVarName; +import static org.keycloak.operator.controllers.KeycloakDistConfigurator.getKeycloakOptionEnvVarName; import static org.keycloak.operator.testsuite.utils.K8sUtils.deployKeycloak; import static org.keycloak.operator.testsuite.utils.K8sUtils.getDefaultKeycloakDeployment; import static org.keycloak.operator.testsuite.utils.K8sUtils.inClusterCurl; @@ -85,7 +85,7 @@ public class RealmImportTest extends BaseOperatorTest { .withImagePullSecrets(new LocalObjectReferenceBuilder().withName("my-empty-secret").build()) .endSpec() .build(); - kc.getSpec().setUnsupported(new KeycloakSpecUnsupported(podTemplate)); + kc.getSpec().setUnsupported(new UnsupportedSpec(podTemplate)); deployKeycloak(k8sclient, kc, false); // Act @@ -118,8 +118,8 @@ public class RealmImportTest extends BaseOperatorTest { var job = k8sclient.batch().v1().jobs().inNamespace(namespace).withName("example-count0-kc").get(); assertThat(job.getSpec().getTemplate().getMetadata().getLabels().get("app")).isEqualTo("keycloak-realm-import"); var envvars = job.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv(); - assertThat(envvars.stream().filter(e -> e.getName().equals(getEnvVarName("cache"))).findAny().get().getValue()).isEqualTo("local"); - assertThat(envvars.stream().filter(e -> e.getName().equals(getEnvVarName("health-enabled"))).findAny().get().getValue()).isEqualTo("false"); + assertThat(envvars.stream().filter(e -> e.getName().equals(getKeycloakOptionEnvVarName("cache"))).findAny().get().getValue()).isEqualTo("local"); + assertThat(envvars.stream().filter(e -> e.getName().equals(getKeycloakOptionEnvVarName("health-enabled"))).findAny().get().getValue()).isEqualTo("false"); assertThat(job.getSpec().getTemplate().getSpec().getImagePullSecrets().size()).isEqualTo(1); assertThat(job.getSpec().getTemplate().getSpec().getImagePullSecrets().get(0).getName()).isEqualTo("my-empty-secret"); 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 202cbccd69..59861b9234 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 @@ -39,7 +39,7 @@ public class CRSerializationTest { assertEquals("my-hostname", keycloak.getSpec().getHostname()); assertEquals("my-image", keycloak.getSpec().getImage()); - assertEquals("my-tls-secret", keycloak.getSpec().getTlsSecret()); + assertEquals("my-tls-secret", keycloak.getSpec().getHttpSpec().getTlsSecret()); assertTrue(keycloak.getSpec().isDisableDefaultIngress()); final TransactionsSpec transactionsSpec = keycloak.getSpec().getTransactionsSpec(); diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/unit/KeycloakDistConfiguratorTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/unit/KeycloakDistConfiguratorTest.java index 5d9576f3ee..50ca36907b 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/unit/KeycloakDistConfiguratorTest.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/unit/KeycloakDistConfiguratorTest.java @@ -25,8 +25,13 @@ import io.quarkus.test.junit.QuarkusTest; import org.junit.jupiter.api.Test; import org.keycloak.common.util.CollectionUtil; import org.keycloak.common.util.ObjectUtil; +import org.keycloak.operator.Constants; import org.keycloak.operator.controllers.KeycloakDistConfigurator; import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; +import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatus; +import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusBuilder; +import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusCondition; +import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret; import org.keycloak.operator.testsuite.utils.K8sUtils; import java.util.Collections; @@ -34,23 +39,54 @@ import java.util.List; import java.util.function.Consumer; import static org.assertj.core.api.Assertions.assertThat; +import static org.keycloak.operator.testsuite.utils.CRAssert.assertKeycloakStatusCondition; +import static org.keycloak.operator.testsuite.utils.CRAssert.assertKeycloakStatusDoesNotContainMessage; @QuarkusTest public class KeycloakDistConfiguratorTest { @Test public void enabledFeatures() { - testFirstClassCitizenEnvVars("KC_FEATURES", KeycloakDistConfigurator::configureFeatures, "docker", "authorization"); + testFirstClassCitizen("KC_FEATURES", "features", + KeycloakDistConfigurator::configureFeatures, "docker", "authorization"); } @Test public void disabledFeatures() { - testFirstClassCitizenEnvVars("KC_FEATURES_DISABLED", KeycloakDistConfigurator::configureFeatures, "admin", "step-up-authentication"); + testFirstClassCitizen("KC_FEATURES_DISABLED", "features-disabled", + KeycloakDistConfigurator::configureFeatures, "admin", "step-up-authentication"); } @Test public void transactions() { - testFirstClassCitizenEnvVars("KC_TRANSACTION_XA_ENABLED", KeycloakDistConfigurator::configureTransactions, "false"); + testFirstClassCitizen("KC_TRANSACTION_XA_ENABLED", "transaction-xa-enabled", + KeycloakDistConfigurator::configureTransactions, "false"); + } + + @Test + public void httpEnabled() { + testFirstClassCitizen("KC_HTTP_ENABLED", "http-enabled", + KeycloakDistConfigurator::configureHttp, "true"); + } + + @Test + public void httpPort() { + 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 @@ -66,11 +102,11 @@ public class KeycloakDistConfiguratorTest { } /* UTILS */ - private void testFirstClassCitizenEnvVars(String varName, Consumer config, String... expectedValues) { - testFirstClassCitizenEnvVars("/test-serialization-keycloak-cr.yml", varName, config, expectedValues); + private void testFirstClassCitizen(String envVarName, String optionName, Consumer config, String... expectedValues) { + testFirstClassCitizen("/test-serialization-keycloak-cr.yml", envVarName, optionName, config, expectedValues); } - private void testFirstClassCitizenEnvVars(String crName, String varName, Consumer config, String... expectedValues) { + private void testFirstClassCitizen(String crName, String envVarName, String optionName, Consumer config, String... expectedValues) { final Keycloak keycloak = K8sUtils.getResourceFromFile(crName, Keycloak.class); final StatefulSet deployment = getBasicKcDeployment(); final KeycloakDistConfigurator distConfig = new KeycloakDistConfigurator(keycloak, deployment, null); @@ -78,11 +114,15 @@ public class KeycloakDistConfiguratorTest { final Container container = deployment.getSpec().getTemplate().getSpec().getContainers().get(0); assertThat(container).isNotNull(); - assertEnvVarNotPresent(container.getEnv(), varName); + assertEnvVarNotPresent(container.getEnv(), envVarName); + assertWarningStatus(distConfig, optionName, false); config.accept(distConfig); - assertContainerEnvVar(container.getEnv(), varName, expectedValues); + assertContainerEnvVar(container.getEnv(), envVarName, expectedValues); + + keycloak.getSpec().setServerConfiguration(List.of(new ValueOrSecret(optionName, "foo"))); + assertWarningStatus(distConfig, optionName, true); } /** @@ -109,6 +149,19 @@ public class KeycloakDistConfiguratorTest { assertThat(containsEnvironmentVariable(envVars, varName)).isFalse(); } + private void assertWarningStatus(KeycloakDistConfigurator distConfig, String optionName, boolean expectWarning) { + final String message = "warning: You need to specify these fields as the first-class citizen of the CR: " + optionName; + final KeycloakStatusBuilder statusBuilder = new KeycloakStatusBuilder(); + distConfig.validateOptions(statusBuilder); + final KeycloakStatus status = statusBuilder.build(); + + if (expectWarning) { + assertKeycloakStatusCondition(status, KeycloakStatusCondition.HAS_ERRORS, false, message); + } else { + assertKeycloakStatusDoesNotContainMessage(status, message); + } + } + private StatefulSet getBasicKcDeployment() { return new StatefulSetBuilder() .withNewSpec() diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/unit/PodTemplateTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/unit/PodTemplateTest.java index 89609b2f4e..ae2a1783fc 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/unit/PodTemplateTest.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/unit/PodTemplateTest.java @@ -29,7 +29,8 @@ import org.keycloak.operator.Config; import org.keycloak.operator.controllers.KeycloakDeployment; import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakSpec; -import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakSpecUnsupported; +import org.keycloak.operator.crds.v2alpha1.deployment.spec.HttpSpec; +import org.keycloak.operator.crds.v2alpha1.deployment.spec.UnsupportedSpec; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -56,9 +57,13 @@ public class PodTemplateTest { }; var kc = new Keycloak(); var spec = new KeycloakSpec(); - spec.setUnsupported(new KeycloakSpecUnsupported(podTemplate)); + spec.setUnsupported(new UnsupportedSpec(podTemplate)); spec.setHostname("example.com"); - spec.setTlsSecret("example-tls-secret"); + + var httpSpec = new HttpSpec(); + httpSpec.setTlsSecret("example-tls-secret"); + spec.setHttpSpec(httpSpec); + kc.setSpec(spec); var deployment = new KeycloakDeployment(null, config, kc, existingDeployment, "dummy-admin"); return (StatefulSet) deployment.getReconciledResource().get(); diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/utils/CRAssert.java b/operator/src/test/java/org/keycloak/operator/testsuite/utils/CRAssert.java index dfa3fdbca2..1c7e6b8fe5 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/utils/CRAssert.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/utils/CRAssert.java @@ -20,6 +20,7 @@ package org.keycloak.operator.testsuite.utils; import io.fabric8.kubernetes.client.utils.Serialization; import io.quarkus.logging.Log; import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; +import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatus; import org.keycloak.operator.crds.v2alpha1.realmimport.KeycloakRealmImport; import static org.assertj.core.api.Assertions.assertThat; @@ -35,20 +36,32 @@ public final class CRAssert { public static void assertKeycloakStatusCondition(Keycloak kc, String condition, boolean status, String containedMessage) { Log.debugf("Asserting CR: %s, condition: %s, status: %s, message: %s", kc.getMetadata().getName(), condition, status, containedMessage); try { - assertThat(kc.getStatus().getConditions().stream() - .anyMatch(c -> - c.getType().equals(condition) && - c.getStatus() == status && - (containedMessage == null || c.getMessage().contains(containedMessage))) - ).isTrue(); + assertKeycloakStatusCondition(kc.getStatus(), condition, status, containedMessage); } catch (Exception e) { Log.infof("Asserting CR: %s with status:\n%s", kc.getMetadata().getName(), Serialization.asYaml(kc.getStatus())); throw e; } } + public static void assertKeycloakStatusCondition(KeycloakStatus kcStatus, String condition, boolean status) { + assertKeycloakStatusCondition(kcStatus, condition, status, null); + } + public static void assertKeycloakStatusCondition(KeycloakStatus kcStatus, String condition, boolean status, String containedMessage) { + assertThat(kcStatus.getConditions()) + .anyMatch(c -> + c.getType().equals(condition) && + c.getStatus() == status && + (containedMessage == null || c.getMessage().contains(containedMessage)) + ); + } + + public static void assertKeycloakStatusDoesNotContainMessage(KeycloakStatus kcStatus, String message) { + assertThat(kcStatus.getConditions()) + .noneMatch(c -> c.getMessage().contains(message)); + } + public static void assertKeycloakRealmImportStatusCondition(KeycloakRealmImport kri, String condition, boolean status) { - assertThat(kri.getStatus().getConditions().stream() - .anyMatch(c -> c.getType().equals(condition) && c.getStatus() == status)).isTrue(); + assertThat(kri.getStatus().getConditions()) + .anyMatch(c -> c.getType().equals(condition) && c.getStatus() == status); } } diff --git a/operator/src/test/resources/correct-podtemplate-keycloak.yml b/operator/src/test/resources/correct-podtemplate-keycloak.yml index 490c40013f..a7d0a13722 100644 --- a/operator/src/test/resources/correct-podtemplate-keycloak.yml +++ b/operator/src/test/resources/correct-podtemplate-keycloak.yml @@ -14,7 +14,6 @@ spec: - name: db-password value: testpassword hostname: example.com - tlsSecret: INSECURE-DISABLE unsupported: podTemplate: metadata: diff --git a/operator/src/test/resources/empty-podtemplate-keycloak.yml b/operator/src/test/resources/empty-podtemplate-keycloak.yml index c7372ca247..bd97bd91dd 100644 --- a/operator/src/test/resources/empty-podtemplate-keycloak.yml +++ b/operator/src/test/resources/empty-podtemplate-keycloak.yml @@ -14,6 +14,5 @@ spec: - name: db-password value: testpassword hostname: example.com - tlsSecret: INSECURE-DISABLE unsupported: podTemplate: diff --git a/operator/src/test/resources/test-serialization-keycloak-cr-with-empty-list.yml b/operator/src/test/resources/test-serialization-keycloak-cr-with-empty-list.yml index f0488adb92..73932680e2 100644 --- a/operator/src/test/resources/test-serialization-keycloak-cr-with-empty-list.yml +++ b/operator/src/test/resources/test-serialization-keycloak-cr-with-empty-list.yml @@ -7,4 +7,5 @@ spec: enabled: - hostname: my-hostname - tlsSecret: my-tls-secret \ No newline at end of file + http: + tlsSecret: my-tls-secret \ No newline at end of file diff --git a/operator/src/test/resources/test-serialization-keycloak-cr.yml b/operator/src/test/resources/test-serialization-keycloak-cr.yml index ac4154d7e0..eca9e2f751 100644 --- a/operator/src/test/resources/test-serialization-keycloak-cr.yml +++ b/operator/src/test/resources/test-serialization-keycloak-cr.yml @@ -11,7 +11,11 @@ spec: - name: features value: docker hostname: my-hostname - tlsSecret: my-tls-secret + http: + httpEnabled: true + httpPort: 123 + httpsPort: 456 + tlsSecret: my-tls-secret features: enabled: - docker