From 2d55e1dab700a486dc98cac211a6755265cabfd3 Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Tue, 18 Oct 2022 16:59:02 -0300 Subject: [PATCH] Add DB options to Keycloak CR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #14374 Co-authored-by: Martin Bartoš --- .../controllers/KeycloakDeployment.java | 4 +- .../controllers/KeycloakDistConfigurator.java | 51 +++++- .../v2alpha1/deployment/KeycloakSpec.java | 17 ++ .../deployment/spec/DatabaseSpec.java | 149 ++++++++++++++++++ .../src/main/resources/example-keycloak.yaml | 22 ++- .../integration/WatchedSecretsTest.java | 3 + .../testsuite/unit/CRSerializationTest.java | 29 ++++ .../unit/KeycloakDistConfiguratorTest.java | 38 ++++- .../test-serialization-keycloak-cr.yml | 16 ++ 9 files changed, 305 insertions(+), 24 deletions(-) create mode 100644 operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/DatabaseSpec.java 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 ae37b15800..b610ace59e 100644 --- a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeployment.java +++ b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeployment.java @@ -518,9 +518,7 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu public Set getConfigSecretsNames() { Set ret = new HashSet<>(serverConfigSecretsNames); - if (isTlsConfigured(keycloakCR)) { - ret.add(keycloakCR.getSpec().getHttpSpec().getTlsSecret()); - } + ret.addAll(distConfigurator.getSecretNames()); return ret; } 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 cd43057ffd..bfbfefe4ae 100644 --- a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDistConfigurator.java +++ b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDistConfigurator.java @@ -19,6 +19,8 @@ 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.EnvVarSourceBuilder; +import io.fabric8.kubernetes.api.model.SecretKeySelector; import io.fabric8.kubernetes.api.model.VolumeBuilder; import io.fabric8.kubernetes.api.model.VolumeMountBuilder; import io.fabric8.kubernetes.api.model.apps.StatefulSet; @@ -29,6 +31,7 @@ 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.ValueOrSecret; +import org.keycloak.operator.crds.v2alpha1.deployment.spec.DatabaseSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.FeatureSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.HttpSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.TransactionsSpec; @@ -38,6 +41,7 @@ import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -72,6 +76,7 @@ public class KeycloakDistConfigurator { configureFeatures(); configureTransactions(); configureHttp(); + configureDatabase(); } /** @@ -165,6 +170,21 @@ public class KeycloakDistConfigurator { kcContainer.getVolumeMounts().add(volumeMount); } + public void configureDatabase() { + optionMapper(keycloakCR.getSpec().getDatabaseSpec()) + .mapOption("db", DatabaseSpec::getVendor) + .mapOption("db-username", DatabaseSpec::getUsernameSecret) + .mapOption("db-password", DatabaseSpec::getPasswordSecret) + .mapOption("db-url-database", DatabaseSpec::getDatabase) + .mapOption("db-url-host", DatabaseSpec::getHost) + .mapOption("db-url-port", DatabaseSpec::getPort) + .mapOption("db-schema", DatabaseSpec::getSchema) + .mapOption("db-url", DatabaseSpec::getUrl) + .mapOption("db-pool-initial-size", DatabaseSpec::getPoolInitialSize) + .mapOption("db-pool-min-size", DatabaseSpec::getPoolMinSize) + .mapOption("db-pool-max-size", DatabaseSpec::getPoolMaxSize); + } + /* ---------- END of configuration of first-class citizen fields ---------- */ /** @@ -196,6 +216,19 @@ public class KeycloakDistConfigurator { return new OptionMapper<>(optionSpec); } + public Collection getSecretNames() { + Set names = new HashSet<>(); + + if (isTlsConfigured(keycloakCR)) { + names.add(keycloakCR.getSpec().getHttpSpec().getTlsSecret()); + } + + Optional.ofNullable(keycloakCR.getSpec().getDatabaseSpec()).map(DatabaseSpec::getUsernameSecret).map(SecretKeySelector::getName).ifPresent(names::add); + Optional.ofNullable(keycloakCR.getSpec().getDatabaseSpec()).map(DatabaseSpec::getPasswordSecret).map(SecretKeySelector::getName).ifPresent(names::add); + + return names; + } + private class OptionMapper { private final T categorySpec; private final List envVars; @@ -221,19 +254,23 @@ public class KeycloakDistConfigurator { } R value = optionValueSupplier.apply(categorySpec); - String valueStr = String.valueOf(value); - if (value == null || valueStr.trim().isEmpty()) { + if (value == null || value.toString().trim().isEmpty()) { Log.debugf("No value provided for %s", optionName); return this; } - EnvVar envVar = new EnvVarBuilder() - .withName(getKeycloakOptionEnvVarName(optionName)) - .withValue(valueStr) - .build(); + EnvVarBuilder envVarBuilder = new EnvVarBuilder() + .withName(getKeycloakOptionEnvVarName(optionName)); - envVars.add(envVar); + if (value instanceof SecretKeySelector) { + envVarBuilder.withValueFrom(new EnvVarSourceBuilder().withSecretKeyRef((SecretKeySelector) value).build()); + } else { + envVarBuilder.withValue(String.valueOf(value)); + } + + + envVars.add(envVarBuilder.build()); return this; } 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 7922c886f8..7556b595d9 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 @@ -22,6 +22,7 @@ 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.DatabaseSpec; 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; @@ -29,6 +30,7 @@ import org.keycloak.operator.crds.v2alpha1.deployment.spec.IngressSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.TransactionsSpec; import javax.validation.constraints.NotNull; +import java.util.ArrayList; import java.util.List; public class KeycloakSpec { @@ -73,6 +75,10 @@ public class KeycloakSpec { @JsonPropertyDescription("In this section you can find all properties related to the settings of transaction behavior.") private TransactionsSpec transactionsSpec; + @JsonProperty("db") + @JsonPropertyDescription("In this section you can find all properties related to connect to a database.") + private DatabaseSpec databaseSpec; + public String getHostname() { return hostname; } @@ -126,6 +132,14 @@ public class KeycloakSpec { this.ingressSpec = ingressSpec; } + public DatabaseSpec getDatabaseSpec() { + return databaseSpec; + } + + public void setDatabaseSpec(DatabaseSpec databaseSpec) { + this.databaseSpec = databaseSpec; + } + public int getInstances() { return instances; } @@ -151,6 +165,9 @@ public class KeycloakSpec { } public List getServerConfiguration() { + if (serverConfiguration == null) { + serverConfiguration = new ArrayList<>(); + } return serverConfiguration; } diff --git a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/DatabaseSpec.java b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/DatabaseSpec.java new file mode 100644 index 0000000000..4fd2a5a71e --- /dev/null +++ b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/DatabaseSpec.java @@ -0,0 +1,149 @@ +/* + * 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.SecretKeySelector; +import io.sundr.builder.annotations.Buildable; + +@Buildable(editableEnabled = false, builderPackage = "io.fabric8.kubernetes.api.builder") +public class DatabaseSpec { + + @JsonPropertyDescription("The database vendor.") + private String vendor; + + @JsonPropertyDescription("The reference to a secret holding the username of the database user.") + private SecretKeySelector usernameSecret; + + @JsonPropertyDescription("The reference to a secret holding the password of the database user.") + private SecretKeySelector passwordSecret; + + @JsonPropertyDescription("Sets the database name of the default JDBC URL of the chosen vendor. If the `url` option is set, this option is ignored.") + private String database; + + @JsonPropertyDescription("Sets the hostname of the default JDBC URL of the chosen vendor. If the `url` option is set, this option is ignored.") + private String host; + + @JsonPropertyDescription("Sets the port of the default JDBC URL of the chosen vendor. If the `url` option is set, this option is ignored.") + private Integer port; + + @JsonPropertyDescription("The database schema to be used.") + private String schema; + + @JsonPropertyDescription("The full database JDBC URL. If not provided, a default URL is set based on the selected database vendor. " + + "For instance, if using 'postgres', the default JDBC URL would be 'jdbc:postgresql://localhost/keycloak'. ") + private String url; + + @JsonPropertyDescription("The initial size of the connection pool.") + private Integer poolInitialSize; + + @JsonPropertyDescription("The minimal size of the connection pool.") + private Integer poolMinSize; + + @JsonPropertyDescription("The maximum size of the connection pool.") + private Integer poolMaxSize; + + public String getVendor() { + return vendor; + } + + public void setVendor(String vendor) { + this.vendor = vendor; + } + + public SecretKeySelector getUsernameSecret() { + return usernameSecret; + } + + public void setUsernameSecret(SecretKeySelector usernameSecret) { + this.usernameSecret = usernameSecret; + } + + public SecretKeySelector getPasswordSecret() { + return passwordSecret; + } + + public void setPasswordSecret(SecretKeySelector passwordSecret) { + this.passwordSecret = passwordSecret; + } + + public String getDatabase() { + return database; + } + + public void setDatabase(String database) { + this.database = database; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + public String getSchema() { + return schema; + } + + public void setSchema(String schema) { + this.schema = schema; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public Integer getPoolInitialSize() { + return poolInitialSize; + } + + public void setPoolInitialSize(Integer poolInitialSize) { + this.poolInitialSize = poolInitialSize; + } + + public Integer getPoolMinSize() { + return poolMinSize; + } + + public void setPoolMinSize(Integer poolMinSize) { + this.poolMinSize = poolMinSize; + } + + public Integer getPoolMaxSize() { + return poolMaxSize; + } + + public void setPoolMaxSize(Integer poolMaxSize) { + this.poolMaxSize = poolMaxSize; + } +} diff --git a/operator/src/main/resources/example-keycloak.yaml b/operator/src/main/resources/example-keycloak.yaml index cafd8555ef..feb610339e 100644 --- a/operator/src/main/resources/example-keycloak.yaml +++ b/operator/src/main/resources/example-keycloak.yaml @@ -4,19 +4,15 @@ metadata: name: example-kc spec: instances: 1 - serverConfiguration: - - name: db - value: postgres - - name: db-url-host - value: postgres-db - - name: db-username - secret: - name: keycloak-db-secret - key: username - - name: db-password - secret: - name: keycloak-db-secret - key: password + db: + vendor: postgres + host: postgres-db + usernameSecret: + name: keycloak-db-secret + key: username + passwordSecret: + name: keycloak-db-secret + key: password hostname: example.com http: tlsSecret: example-tls-secret \ No newline at end of file diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/integration/WatchedSecretsTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/integration/WatchedSecretsTest.java index 03f7ac7662..e0349b8326 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/integration/WatchedSecretsTest.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/integration/WatchedSecretsTest.java @@ -269,6 +269,9 @@ public class WatchedSecretsTest extends BaseOperatorTest { } private void hardcodeDBCredsInCR(Keycloak kc) { + kc.getSpec().getDatabaseSpec().setUsernameSecret(null); + kc.getSpec().getDatabaseSpec().setPasswordSecret(null); + var username = new ValueOrSecret("db-username", "postgres"); var password = new ValueOrSecret("db-password", "testpassword"); 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 f6f89beb69..02f44878ef 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 @@ -19,17 +19,24 @@ package org.keycloak.operator.testsuite.unit; import io.fabric8.kubernetes.client.utils.Serialization; import org.hamcrest.CoreMatchers; +import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; +import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret; +import org.keycloak.operator.crds.v2alpha1.deployment.spec.DatabaseSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.FeatureSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.TransactionsSpec; import java.util.List; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasProperty; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; public class CRSerializationTest { @@ -46,6 +53,28 @@ public class CRSerializationTest { assertThat(transactionsSpec, notNullValue()); assertThat(transactionsSpec.isXaEnabled(), notNullValue()); assertThat(transactionsSpec.isXaEnabled(), CoreMatchers.is(false)); + + List serverConfiguration = keycloak.getSpec().getServerConfiguration(); + + assertNotNull(serverConfiguration); + assertFalse(serverConfiguration.isEmpty()); + assertThat(serverConfiguration, hasItem(hasProperty("name", is("key1")))); + + DatabaseSpec databaseSpec = keycloak.getSpec().getDatabaseSpec(); + assertNotNull(databaseSpec); + assertEquals("vendor", databaseSpec.getVendor()); + assertEquals("database", databaseSpec.getDatabase()); + assertEquals("host", databaseSpec.getHost()); + assertEquals(123, databaseSpec.getPort()); + assertEquals("url", databaseSpec.getUrl()); + assertEquals("schema", databaseSpec.getSchema()); + assertEquals(1, databaseSpec.getPoolInitialSize()); + assertEquals(2, databaseSpec.getPoolMinSize()); + assertEquals(3, databaseSpec.getPoolMaxSize()); + assertEquals("usernameSecret", databaseSpec.getUsernameSecret().getName()); + assertEquals("usernameSecretKey", databaseSpec.getUsernameSecret().getKey()); + assertEquals("passwordSecret", databaseSpec.getPasswordSecret().getName()); + assertEquals("passwordSecretKey", databaseSpec.getPasswordSecret().getKey()); } @Test 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 50ca36907b..cc4d493374 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 @@ -37,6 +37,7 @@ import org.keycloak.operator.testsuite.utils.K8sUtils; import java.util.Collections; import java.util.List; import java.util.function.Consumer; +import java.util.function.Function; import static org.assertj.core.api.Assertions.assertThat; import static org.keycloak.operator.testsuite.utils.CRAssert.assertKeycloakStatusCondition; @@ -101,6 +102,28 @@ public class KeycloakDistConfiguratorTest { assertEnvVarNotPresent(envVars, "KC_FEATURES_DISABLED"); } + @Test + public void testDatabaseSettings() { + testFirstClassCitizen("KC_DB", "db", + KeycloakDistConfigurator::configureDatabase, "vendor"); + testFirstClassCitizen("KC_DB_USERNAME", "db-username", + KeycloakDistConfigurator::configureDatabase, "usernameSecret"); + testFirstClassCitizen("KC_DB_PASSWORD", "db-password", + KeycloakDistConfigurator::configureDatabase, "passwordSecret"); + testFirstClassCitizen("KC_DB_SCHEMA", "db-schema", + KeycloakDistConfigurator::configureDatabase, "schema"); + testFirstClassCitizen("KC_DB_URL_HOST", "db-url-host", + KeycloakDistConfigurator::configureDatabase, "host"); + testFirstClassCitizen("KC_DB_URL_PORT", "db-url-port", + KeycloakDistConfigurator::configureDatabase, "123"); + testFirstClassCitizen("KC_DB_POOL_INITIAL_SIZE", "db-pool-initial-size", + KeycloakDistConfigurator::configureDatabase, "1"); + testFirstClassCitizen("KC_DB_POOL_MIN_SIZE", "db-pool-min-size", + KeycloakDistConfigurator::configureDatabase, "2"); + testFirstClassCitizen("KC_DB_POOL_MAX_SIZE", "db-pool-max-size", + KeycloakDistConfigurator::configureDatabase, "3"); + } + /* UTILS */ private void testFirstClassCitizen(String envVarName, String optionName, Consumer config, String... expectedValues) { testFirstClassCitizen("/test-serialization-keycloak-cr.yml", envVarName, optionName, config, expectedValues); @@ -190,7 +213,20 @@ public class KeycloakDistConfiguratorTest { return envVars.stream().filter(f -> varName.equals(f.getName())) .findFirst() - .map(EnvVar::getValue) + .map(new Function() { + @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); diff --git a/operator/src/test/resources/test-serialization-keycloak-cr.yml b/operator/src/test/resources/test-serialization-keycloak-cr.yml index 059e134ab8..3eaf493d1f 100644 --- a/operator/src/test/resources/test-serialization-keycloak-cr.yml +++ b/operator/src/test/resources/test-serialization-keycloak-cr.yml @@ -11,6 +11,22 @@ spec: - name: features value: docker hostname: my-hostname + db: + vendor: vendor + usernameSecret: + name: usernameSecret + key: usernameSecretKey + passwordSecret: + name: passwordSecret + key: passwordSecretKey + host: host + database: database + url: url + port: 123 + schema: schema + poolInitialSize: 1 + poolMinSize: 2 + poolMaxSize: 3 ingress: enabled: false http: