Skip to content

Commit

Permalink
Added field to the RealmImport spec to replace environment variables …
Browse files Browse the repository at this point in the history
…within the realm import (keycloak#31232)

* Added field to the RealmImport spec to replace environment variables within the realm import

Closes keycloak#26470

Signed-off-by: stustison <[email protected]>

* Added field to the RealmImport spec to replace environment variables within the realm import

Closes keycloak#26470

Signed-off-by: stustison <[email protected]>

* testing refinement for placeholder handling

closes: keycloak#26470

Signed-off-by: Steve Hawkins <[email protected]>

* changing from placeholdersecret to placeholder

Signed-off-by: Steve Hawkins <[email protected]>

* Update docs/guides/operator/realm-import.adoc

Co-authored-by: Martin Bartoš <[email protected]>
Signed-off-by: Steven Hawkins <[email protected]>

* Update docs/documentation/release_notes/topics/26_0_0.adoc

Co-authored-by: Martin Bartoš <[email protected]>
Signed-off-by: Steven Hawkins <[email protected]>

---------

Signed-off-by: stustison <[email protected]>
Signed-off-by: Steve Hawkins <[email protected]>
Signed-off-by: Steven Hawkins <[email protected]>
Co-authored-by: stustison <[email protected]>
Co-authored-by: Martin Bartoš <[email protected]>
  • Loading branch information
3 people authored Jul 29, 2024
1 parent 28a27c9 commit 22f8e5c
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 5 deletions.
7 changes: 7 additions & 0 deletions docs/documentation/release_notes/topics/26_0_0.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
24 changes: 24 additions & 0 deletions docs/guides/operator/realm-import.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,14 @@
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;
Expand All @@ -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()
Expand All @@ -68,7 +71,7 @@ protected Job desired(KeycloakRealmImport primary, Context<KeycloakRealmImport>
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();
Expand All @@ -93,6 +96,22 @@ protected Job desired(KeycloakRealmImport primary, Context<KeycloakRealmImport>
// 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);
}

Expand Down Expand Up @@ -121,7 +140,7 @@ private Volume buildSecretVolume(String volumeName, String secretName) {
.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");
Expand All @@ -130,8 +149,10 @@ private void buildKeycloakJobContainer(Container keycloakContainer, KeycloakReal

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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}
Expand All @@ -58,4 +68,12 @@ public ResourceRequirements getResourceRequirements() {
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;
}
}
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -34,10 +37,13 @@
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;

Expand All @@ -50,6 +56,7 @@
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
Expand All @@ -68,6 +75,12 @@ public void beforeEach(TestInfo testInfo) {
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
Expand All @@ -87,10 +100,15 @@ private String getJobArgs() {
.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);
Expand All @@ -99,6 +117,38 @@ public void testWorkingRealmImport() {
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)
Expand Down Expand Up @@ -131,6 +181,7 @@ public void testWorkingRealmImport() {
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");

Expand All @@ -148,6 +199,8 @@ public void testWorkingRealmImport() {
});

assertThat(getJobArgs()).contains("build");

return envvars;
}

@Test
Expand Down
14 changes: 14 additions & 0 deletions operator/src/test/resources/example-smtp-secret.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
apiVersion: v1
kind: Secret
metadata:
name: keycloak-smtp-secret
stringData:
SMTP_PORT: "1234"
SMTP_SERVER: "example.com"
SMTP_FROM: "[email protected]"
SMTP_REPLY: "[email protected]"
SMTP_USE_TLS: "true"
SMTP_USERNAME: "example"
SMTP_PASSWORD: "example"
SMTP_AUTH: "true"
type: Opaque

0 comments on commit 22f8e5c

Please sign in to comment.