Add DB options to Keycloak CR

Closes #14374

Co-authored-by: Martin Bartoš <mabartos@redhat.com>
This commit is contained in:
Pedro Igor 2022-10-18 16:59:02 -03:00 committed by Václav Muzikář
parent e712cd6a0e
commit 2d55e1dab7
9 changed files with 305 additions and 24 deletions

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -4,17 +4,13 @@ metadata:
name: example-kc
spec:
instances: 1
serverConfiguration:
- name: db
value: postgres
- name: db-url-host
value: postgres-db
- name: db-username
secret:
db:
vendor: postgres
host: postgres-db
usernameSecret:
name: keycloak-db-secret
key: username
- name: db-password
secret:
passwordSecret:
name: keycloak-db-secret
key: password
hostname: example.com

View file

@ -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");

View file

@ -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

View file

@ -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);

View file

@ -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: