Add DB options to Keycloak CR
Closes #14374 Co-authored-by: Martin Bartoš <mabartos@redhat.com>
This commit is contained in:
parent
e712cd6a0e
commit
2d55e1dab7
9 changed files with 305 additions and 24 deletions
|
@ -518,9 +518,7 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu
|
|||
|
||||
public Set<String> getConfigSecretsNames() {
|
||||
Set<String> ret = new HashSet<>(serverConfigSecretsNames);
|
||||
if (isTlsConfigured(keycloakCR)) {
|
||||
ret.add(keycloakCR.getSpec().getHttpSpec().getTlsSecret());
|
||||
}
|
||||
ret.addAll(distConfigurator.getSecretNames());
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
|
|
@ -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<String> getSecretNames() {
|
||||
Set<String> 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<T> {
|
||||
private final T categorySpec;
|
||||
private final List<EnvVar> 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;
|
||||
}
|
||||
|
|
|
@ -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<ValueOrSecret> getServerConfiguration() {
|
||||
if (serverConfiguration == null) {
|
||||
serverConfiguration = new ArrayList<>();
|
||||
}
|
||||
return serverConfiguration;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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<ValueOrSecret> 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
|
||||
|
|
|
@ -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<KeycloakDistConfigurator> 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<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);
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue