mirror of
https://github.com/keycloak/keycloak.git
synced 2026-06-09 00:52:07 -04:00
Added field to the RealmImport spec to replace environment variables within the realm import (#31232)
* Added field to the RealmImport spec to replace environment variables within the realm import Closes #26470 Signed-off-by: stustison <scott.tustison@gmail.com> * Added field to the RealmImport spec to replace environment variables within the realm import Closes #26470 Signed-off-by: stustison <scott.tustison@gmail.com> * testing refinement for placeholder handling closes: #26470 Signed-off-by: Steve Hawkins <shawkins@redhat.com> * changing from placeholdersecret to placeholder Signed-off-by: Steve Hawkins <shawkins@redhat.com> * Update docs/guides/operator/realm-import.adoc Co-authored-by: Martin Bartoš <mabartos@redhat.com> Signed-off-by: Steven Hawkins <shawkins@redhat.com> * Update docs/documentation/release_notes/topics/26_0_0.adoc Co-authored-by: Martin Bartoš <mabartos@redhat.com> Signed-off-by: Steven Hawkins <shawkins@redhat.com> --------- Signed-off-by: stustison <scott.tustison@gmail.com> Signed-off-by: Steve Hawkins <shawkins@redhat.com> Signed-off-by: Steven Hawkins <shawkins@redhat.com> Co-authored-by: stustison <scott.tustison@gmail.com> Co-authored-by: Martin Bartoš <mabartos@redhat.com>
This commit is contained in:
parent
28a27c9148
commit
22f8e5cdf0
7 changed files with 203 additions and 5 deletions
|
|
@ -43,6 +43,13 @@ The Keycloak CR now exposes first class properties for controlling the schedulin
|
|||
For more details, see the
|
||||
https://www.keycloak.org/operator/advanced-configuration[Operator Advanced Configuration].
|
||||
|
||||
= KeycloakRealmImport CR supports placeholder replacement
|
||||
|
||||
The KeycloakRealmImport CR now exposes `spec.placeholders` to create environment variables for placeholder replacement in the import.
|
||||
|
||||
For more details, see the
|
||||
https://www.keycloak.org/operator/realm-import[Operator Realm Import].
|
||||
|
||||
= Configuring the LDAP Connection Pool
|
||||
|
||||
In this release, the LDAP connection pool configuration relies solely on system properties.
|
||||
|
|
|
|||
|
|
@ -94,4 +94,28 @@ CONDITION: HasErrors
|
|||
MESSAGE:
|
||||
----
|
||||
|
||||
=== Placeholders
|
||||
|
||||
Imports support placeholders referencing environment variables, see <@links.server id="importExport"/> for more.
|
||||
The `KeycloakRealmImport` CR allows you to leverage this functionality via the `spec.placeholders` stanza, for example:
|
||||
|
||||
[source,yaml]
|
||||
----
|
||||
apiVersion: k8s.keycloak.org/v2alpha1
|
||||
kind: KeycloakRealmImport
|
||||
metadata:
|
||||
name: my-realm-kc
|
||||
spec:
|
||||
keycloakCRName: <name of the keycloak CR>
|
||||
placeholders:
|
||||
ENV_KEY:
|
||||
secret:
|
||||
name: SECRET_NAME
|
||||
key: SECRET_KEY
|
||||
...
|
||||
----
|
||||
|
||||
In the above example placeholder replacement will be enabled and an environment variable with key `ENV_KEY` will be created from the Secret `SECRET_NAME`'s value for key `SECRET_KEY`.
|
||||
Currently only Secrets are supported and they must be in the same namespace as the Keycloak CR.
|
||||
|
||||
</@tmpl.guide>
|
||||
|
|
|
|||
|
|
@ -33,13 +33,14 @@ import io.javaoperatorsdk.operator.processing.dependent.Creator;
|
|||
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource;
|
||||
|
||||
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfigBuilder;
|
||||
import jakarta.inject.Inject;
|
||||
import org.keycloak.operator.Config;
|
||||
import org.keycloak.operator.Constants;
|
||||
import org.keycloak.operator.Utils;
|
||||
import org.keycloak.operator.crds.v2alpha1.realmimport.KeycloakRealmImport;
|
||||
import org.keycloak.operator.crds.v2alpha1.realmimport.Placeholder;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.keycloak.operator.Utils.addResources;
|
||||
|
|
@ -60,6 +61,8 @@ public class KeycloakRealmImportJobDependentResource extends KubernetesDependent
|
|||
@Override
|
||||
protected Job desired(KeycloakRealmImport primary, Context<KeycloakRealmImport> context) {
|
||||
StatefulSet existingDeployment = context.managedDependentResourceContext().get(StatefulSet.class, StatefulSet.class).orElseThrow();
|
||||
Map<String, Placeholder> placeholders = primary.getSpec().getPlaceholders();
|
||||
boolean replacePlaceholders = (placeholders != null && !placeholders.isEmpty());
|
||||
|
||||
var keycloakPodTemplate = existingDeployment
|
||||
.getSpec()
|
||||
|
|
@ -68,7 +71,7 @@ public class KeycloakRealmImportJobDependentResource extends KubernetesDependent
|
|||
String secretName = KeycloakRealmImportSecretDependentResource.getSecretName(primary);
|
||||
String volumeName = KubernetesResourceUtil.sanitizeName(secretName + "-volume");
|
||||
|
||||
buildKeycloakJobContainer(keycloakPodTemplate.getSpec().getContainers().get(0), primary, volumeName);
|
||||
buildKeycloakJobContainer(keycloakPodTemplate.getSpec().getContainers().get(0), primary, volumeName, replacePlaceholders);
|
||||
keycloakPodTemplate.getSpec().getVolumes().add(buildSecretVolume(volumeName, secretName));
|
||||
|
||||
var labels = keycloakPodTemplate.getMetadata().getLabels();
|
||||
|
|
@ -93,6 +96,22 @@ public class KeycloakRealmImportJobDependentResource extends KubernetesDependent
|
|||
// The Job doesn't need health to be enabled
|
||||
envvars.add(new EnvVarBuilder().withName(healthEnvVarName).withValue("false").build());
|
||||
|
||||
if (replacePlaceholders) {
|
||||
for (Map.Entry<String, Placeholder> secret : primary.getSpec().getPlaceholders().entrySet()) {
|
||||
envvars.add(
|
||||
new EnvVarBuilder()
|
||||
.withName(secret.getKey())
|
||||
.withNewValueFrom()
|
||||
.withNewSecretKeyRef()
|
||||
.withName(secret.getValue().getSecret().getName())
|
||||
.withKey(secret.getValue().getSecret().getKey())
|
||||
.withOptional(false)
|
||||
.endSecretKeyRef()
|
||||
.endValueFrom()
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
return buildJob(keycloakPodTemplate, primary);
|
||||
}
|
||||
|
||||
|
|
@ -121,7 +140,7 @@ public class KeycloakRealmImportJobDependentResource extends KubernetesDependent
|
|||
.build();
|
||||
}
|
||||
|
||||
private void buildKeycloakJobContainer(Container keycloakContainer, KeycloakRealmImport keycloakRealmImport, String volumeName) {
|
||||
private void buildKeycloakJobContainer(Container keycloakContainer, KeycloakRealmImport keycloakRealmImport, String volumeName, boolean replacePlaceholders) {
|
||||
var importMntPath = "/mnt/realm-import/";
|
||||
|
||||
var command = List.of("/bin/bash");
|
||||
|
|
@ -130,8 +149,10 @@ public class KeycloakRealmImportJobDependentResource extends KubernetesDependent
|
|||
|
||||
var runBuild = !keycloakContainer.getArgs().contains(KeycloakDeploymentDependentResource.OPTIMIZED_ARG) ? "/opt/keycloak/bin/kc.sh --verbose build && " : "";
|
||||
|
||||
var replaceOption = (replacePlaceholders) ? " -Dkeycloak.migration.replace-placeholders=true": "";
|
||||
|
||||
var commandArgs = List.of("-c",
|
||||
runBuild + "/opt/keycloak/bin/kc.sh --verbose import --optimized --file='" + importMntPath + keycloakRealmImport.getRealmName() + "-realm.json' " + override);
|
||||
runBuild + "/opt/keycloak/bin/kc.sh" + replaceOption + " --verbose import --optimized --file='" + importMntPath + keycloakRealmImport.getRealmName() + "-realm.json' " + override);
|
||||
|
||||
keycloakContainer.setCommand(command);
|
||||
keycloakContainer.setArgs(commandArgs);
|
||||
|
|
|
|||
|
|
@ -16,12 +16,19 @@
|
|||
*/
|
||||
package org.keycloak.operator.crds.v2alpha1.realmimport;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
|
||||
import io.fabric8.generator.annotation.Required;
|
||||
import io.fabric8.kubernetes.api.model.ResourceRequirements;
|
||||
import io.sundr.builder.annotations.Buildable;
|
||||
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@Buildable(editableEnabled = false, builderPackage = "io.fabric8.kubernetes.api.builder")
|
||||
public class KeycloakRealmImportSpec {
|
||||
|
||||
@Required
|
||||
|
|
@ -35,6 +42,9 @@ public class KeycloakRealmImportSpec {
|
|||
@JsonPropertyDescription("Compute Resources required by Keycloak container. If not specified, the value is inherited from the Keycloak CR.")
|
||||
private ResourceRequirements resourceRequirements;
|
||||
|
||||
@JsonPropertyDescription("Optionally set to replace ENV variable placeholders in the realm import.")
|
||||
private Map<String, Placeholder> placeholders;
|
||||
|
||||
public String getKeycloakCRName() {
|
||||
return keycloakCRName;
|
||||
}
|
||||
|
|
@ -58,4 +68,12 @@ public class KeycloakRealmImportSpec {
|
|||
public void setResourceRequirements(ResourceRequirements resourceRequirements) {
|
||||
this.resourceRequirements = resourceRequirements;
|
||||
}
|
||||
|
||||
public Map<String, Placeholder> getPlaceholders() {
|
||||
return placeholders;
|
||||
}
|
||||
|
||||
public void setPlaceholders(Map<String, Placeholder> placeholders) {
|
||||
this.placeholders = placeholders;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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.realmimport;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import io.fabric8.kubernetes.api.model.SecretKeySelector;
|
||||
import io.sundr.builder.annotations.Buildable;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @author Scott Tustison
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@Buildable(editableEnabled = false, builderPackage = "io.fabric8.kubernetes.api.builder")
|
||||
public class Placeholder {
|
||||
private SecretKeySelector secret;
|
||||
|
||||
public Placeholder() {
|
||||
}
|
||||
|
||||
public Placeholder(SecretKeySelector secret) {
|
||||
this.secret = secret;
|
||||
}
|
||||
|
||||
public SecretKeySelector getSecret() {
|
||||
return secret;
|
||||
}
|
||||
|
||||
public void setSecret(SecretKeySelector secret) {
|
||||
this.secret = secret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Placeholder that = (Placeholder) o;
|
||||
return getSecret().equals(that.getSecret());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(getSecret());
|
||||
}
|
||||
}
|
||||
|
|
@ -18,12 +18,15 @@
|
|||
package org.keycloak.operator.testsuite.integration;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.Container;
|
||||
import io.fabric8.kubernetes.api.model.EnvVar;
|
||||
import io.fabric8.kubernetes.api.model.LocalObjectReferenceBuilder;
|
||||
import io.fabric8.kubernetes.api.model.Quantity;
|
||||
import io.fabric8.kubernetes.api.model.ResourceRequirements;
|
||||
import io.fabric8.kubernetes.api.model.Secret;
|
||||
import io.fabric8.kubernetes.api.model.SecretKeySelectorBuilder;
|
||||
import io.quarkus.logging.Log;
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
|
||||
import io.quarkus.test.junit.callback.QuarkusTestMethodContext;
|
||||
import jakarta.inject.Inject;
|
||||
import org.awaitility.Awaitility;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
|
@ -34,10 +37,13 @@ import org.keycloak.operator.Config;
|
|||
import org.keycloak.operator.controllers.KeycloakServiceDependentResource;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
|
||||
import org.keycloak.operator.crds.v2alpha1.realmimport.KeycloakRealmImport;
|
||||
import org.keycloak.operator.crds.v2alpha1.realmimport.KeycloakRealmImportBuilder;
|
||||
import org.keycloak.operator.crds.v2alpha1.realmimport.Placeholder;
|
||||
import org.keycloak.operator.testsuite.utils.CRAssert;
|
||||
import org.keycloak.operator.testsuite.utils.K8sUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
|
@ -50,6 +56,7 @@ import static org.keycloak.operator.crds.v2alpha1.realmimport.KeycloakRealmImpor
|
|||
import static org.keycloak.operator.crds.v2alpha1.realmimport.KeycloakRealmImportStatusCondition.HAS_ERRORS;
|
||||
import static org.keycloak.operator.crds.v2alpha1.realmimport.KeycloakRealmImportStatusCondition.STARTED;
|
||||
import static org.keycloak.operator.testsuite.utils.K8sUtils.deployKeycloak;
|
||||
import static org.keycloak.operator.testsuite.utils.K8sUtils.getResourceFromFile;
|
||||
import static org.keycloak.operator.testsuite.utils.K8sUtils.inClusterCurl;
|
||||
|
||||
@QuarkusTest
|
||||
|
|
@ -68,6 +75,12 @@ public class RealmImportTest extends BaseOperatorTest {
|
|||
deleteDB();
|
||||
deployDB();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterEach(QuarkusTestMethodContext context) {
|
||||
super.afterEach(context);
|
||||
k8sclient.resource(getResourceFromFile("example-smtp-secret.yaml", Secret.class)).delete();
|
||||
}
|
||||
|
||||
private String getJobArgs() {
|
||||
return k8sclient
|
||||
|
|
@ -87,10 +100,15 @@ public class RealmImportTest extends BaseOperatorTest {
|
|||
.collect(Collectors.joining());
|
||||
}
|
||||
|
||||
protected static void deploySmtpSecret() {
|
||||
K8sUtils.set(k8sclient, getResourceFromFile("example-smtp-secret.yaml", Secret.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWorkingRealmImport() {
|
||||
// Arrange
|
||||
var kc = getTestKeycloakDeployment(false);
|
||||
|
||||
kc.getSpec().setImage(null); // checks the job args for the base, not custom image
|
||||
kc.getSpec().setImagePullSecrets(Arrays.asList(new LocalObjectReferenceBuilder().withName("my-empty-secret").build()));
|
||||
deployKeycloak(k8sclient, kc, false);
|
||||
|
|
@ -99,6 +117,38 @@ public class RealmImportTest extends BaseOperatorTest {
|
|||
K8sUtils.set(k8sclient, getClass().getResourceAsStream("/example-realm.yaml"));
|
||||
|
||||
// Assert
|
||||
assertWorkingRealmImport(kc);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWorkingRealmImportWithReplacement() {
|
||||
// Arrange
|
||||
var kc = getTestKeycloakDeployment(false);
|
||||
|
||||
deploySmtpSecret();
|
||||
|
||||
kc.getSpec().setImage(null); // checks the job args for the base, not custom image
|
||||
kc.getSpec().setImagePullSecrets(Arrays.asList(new LocalObjectReferenceBuilder().withName("my-empty-secret").build()));
|
||||
deployKeycloak(k8sclient, kc, false);
|
||||
|
||||
// Act
|
||||
k8sclient.getKubernetesSerialization().registerKubernetesResource(KeycloakRealmImport.class);
|
||||
K8sUtils.set(k8sclient, getClass().getResourceAsStream("/example-realm.yaml"), obj -> {
|
||||
KeycloakRealmImport realmImport = (KeycloakRealmImport) obj;
|
||||
realmImport.getSpec().getRealm().setSmtpServer(Map.of("port", "${MY_SMTP_PORT}", "host", "${MY_SMTP_SERVER}"));
|
||||
realmImport.getSpec().setPlaceholders(Map.of("MY_SMTP_PORT", new Placeholder(new SecretKeySelectorBuilder().withName("keycloak-smtp-secret").withKey("SMTP_PORT").build()),
|
||||
"MY_SMTP_SERVER", new Placeholder(new SecretKeySelectorBuilder().withName("keycloak-smtp-secret").withKey("SMTP_SERVER").build())));
|
||||
return realmImport;
|
||||
});
|
||||
|
||||
// Assert
|
||||
var envvars = assertWorkingRealmImport(kc);
|
||||
|
||||
assertThat(envvars.stream().filter(e -> e.getName().equals("MY_SMTP_PORT")).findAny().get().getValueFrom().getSecretKeyRef().getKey()).isEqualTo("SMTP_PORT");
|
||||
assertThat(envvars.stream().filter(e -> e.getName().equals("MY_SMTP_SERVER")).findAny().get().getValueFrom().getSecretKeyRef().getKey()).isEqualTo("SMTP_SERVER");
|
||||
}
|
||||
|
||||
private List<EnvVar> assertWorkingRealmImport(Keycloak kc) {
|
||||
var crSelector = k8sclient
|
||||
.resources(KeycloakRealmImport.class)
|
||||
.inNamespace(namespace)
|
||||
|
|
@ -131,6 +181,7 @@ public class RealmImportTest extends BaseOperatorTest {
|
|||
var envvars = container.getEnv();
|
||||
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");
|
||||
|
||||
|
|
@ -148,6 +199,8 @@ public class RealmImportTest extends BaseOperatorTest {
|
|||
});
|
||||
|
||||
assertThat(getJobArgs()).contains("build");
|
||||
|
||||
return envvars;
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
14
operator/src/test/resources/example-smtp-secret.yaml
Normal file
14
operator/src/test/resources/example-smtp-secret.yaml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: keycloak-smtp-secret
|
||||
stringData:
|
||||
SMTP_PORT: "1234"
|
||||
SMTP_SERVER: "example.com"
|
||||
SMTP_FROM: "example@example.com"
|
||||
SMTP_REPLY: "example@example.com"
|
||||
SMTP_USE_TLS: "true"
|
||||
SMTP_USERNAME: "example"
|
||||
SMTP_PASSWORD: "example"
|
||||
SMTP_AUTH: "true"
|
||||
type: Opaque
|
||||
Loading…
Reference in a new issue