Initial bootstrap admin

This commit is contained in:
andreaTP 2022-03-08 18:29:50 +00:00 committed by Bruno Oliveira da Silva
parent 6621fb3988
commit 59450948f4
6 changed files with 162 additions and 4 deletions

View file

@ -0,0 +1,47 @@
package org.keycloak.operator.v2alpha1;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.Secret;
import io.fabric8.kubernetes.api.model.SecretBuilder;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil;
import org.keycloak.operator.OperatorManagedResource;
import org.keycloak.operator.v2alpha1.crds.Keycloak;
import java.util.Optional;
import java.util.UUID;
public class KeycloakAdminSecret extends OperatorManagedResource {
private final String secretName;
public KeycloakAdminSecret(KubernetesClient client, Keycloak keycloak) {
super(client, keycloak);
this.secretName = KubernetesResourceUtil.sanitizeName(keycloak.getMetadata().getName() + "-initial-admin");
}
@Override
protected Optional<HasMetadata> getReconciledResource() {
if (client.secrets().inNamespace(getNamespace()).withName(secretName).get() != null) {
return Optional.empty();
} else {
return Optional.of(createSecret());
}
}
private Secret createSecret() {
return new SecretBuilder()
.withNewMetadata()
.withName(secretName)
.withNamespace(getNamespace())
.endMetadata()
.withType("kubernetes.io/basic-auth")
.addToStringData("username", "admin")
.addToStringData("password", UUID.randomUUID().toString().replace("-", ""))
.build();
}
@Override
public String getName() { return secretName; }
}

View file

@ -91,9 +91,12 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
var statusBuilder = new KeycloakStatusBuilder(); var statusBuilder = new KeycloakStatusBuilder();
var kcAdminSecret = new KeycloakAdminSecret(client, kc);
kcAdminSecret.createOrUpdateReconciled();
// TODO use caches in secondary resources; this is a workaround for https://github.com/java-operator-sdk/java-operator-sdk/issues/830 // TODO use caches in secondary resources; this is a workaround for https://github.com/java-operator-sdk/java-operator-sdk/issues/830
// KeycloakDeployment deployment = new KeycloakDeployment(client, config, kc, context.getSecondaryResource(Deployment.class).orElse(null)); // KeycloakDeployment deployment = new KeycloakDeployment(client, config, kc, context.getSecondaryResource(Deployment.class).orElse(null));
var kcDeployment = new KeycloakDeployment(client, config, kc, null); var kcDeployment = new KeycloakDeployment(client, config, kc, null, kcAdminSecret.getName());
kcDeployment.updateStatus(statusBuilder); kcDeployment.updateStatus(statusBuilder);
kcDeployment.createOrUpdateReconciled(); kcDeployment.createOrUpdateReconciled();

View file

@ -54,11 +54,13 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu
private final Keycloak keycloakCR; private final Keycloak keycloakCR;
private final Deployment existingDeployment; private final Deployment existingDeployment;
private final Deployment baseDeployment; private final Deployment baseDeployment;
private final String adminSecretName;
public KeycloakDeployment(KubernetesClient client, Config config, Keycloak keycloakCR, Deployment existingDeployment) { public KeycloakDeployment(KubernetesClient client, Config config, Keycloak keycloakCR, Deployment existingDeployment, String adminSecretName) {
super(client, keycloakCR); super(client, keycloakCR);
this.config = config; this.config = config;
this.keycloakCR = keycloakCR; this.keycloakCR = keycloakCR;
this.adminSecretName = adminSecretName;
if (existingDeployment != null) { if (existingDeployment != null) {
Log.info("Existing Deployment provided by controller"); Log.info("Existing Deployment provided by controller");
@ -483,12 +485,37 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu
if (keycloakCR.getSpec().getServerConfiguration() != null) { if (keycloakCR.getSpec().getServerConfiguration() != null) {
serverConfig.putAll(keycloakCR.getSpec().getServerConfiguration()); serverConfig.putAll(keycloakCR.getSpec().getServerConfiguration());
} }
return serverConfig.entrySet().stream() var envVars = serverConfig.entrySet().stream()
.map(e -> new EnvVarBuilder() .map(e -> new EnvVarBuilder()
.withName(e.getKey()) .withName(e.getKey())
.withValue(e.getValue()) .withValue(e.getValue())
.build()) .build())
.collect(Collectors.toList()); .collect(Collectors.toList());
envVars.add(
new EnvVarBuilder()
.withName("KEYCLOAK_ADMIN")
.withNewValueFrom()
.withNewSecretKeyRef()
.withName(this.adminSecretName)
.withKey("username")
.withOptional(false)
.endSecretKeyRef()
.endValueFrom()
.build());
envVars.add(
new EnvVarBuilder()
.withName("KEYCLOAK_ADMIN_PASSWORD")
.withNewValueFrom()
.withNewSecretKeyRef()
.withName(this.adminSecretName)
.withKey("password")
.withOptional(false)
.endSecretKeyRef()
.endValueFrom()
.build());
return envVars;
} }
public void updateStatus(KeycloakStatusBuilder status) { public void updateStatus(KeycloakStatusBuilder status) {

View file

@ -163,6 +163,11 @@ public abstract class ClusterOperatorTest {
.untilAsserted(() -> assertThat(k8sclient.apps().statefulSets().inNamespace(namespace).withName("postgresql-db").get().getStatus().getReadyReplicas()).isEqualTo(1)); .untilAsserted(() -> assertThat(k8sclient.apps().statefulSets().inNamespace(namespace).withName("postgresql-db").get().getStatus().getReadyReplicas()).isEqualTo(1));
} }
protected static void deleteDB() {
// Delete the Postgres StatefulSet
k8sclient.apps().statefulSets().inNamespace(namespace).withName("postgresql-db").delete();
}
// TODO improve this (preferably move to JOSDK) // TODO improve this (preferably move to JOSDK)
protected void savePodLogs() { protected void savePodLogs() {
Log.infof("Saving pod logs to %s", POD_LOGS_DIR); Log.infof("Saving pod logs to %s", POD_LOGS_DIR);

View file

@ -7,16 +7,21 @@ import io.quarkus.test.junit.QuarkusTest;
import org.awaitility.Awaitility; import org.awaitility.Awaitility;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.keycloak.operator.utils.K8sUtils; import org.keycloak.operator.utils.K8sUtils;
import org.keycloak.operator.v2alpha1.KeycloakAdminSecret;
import org.keycloak.operator.v2alpha1.KeycloakService; import org.keycloak.operator.v2alpha1.KeycloakService;
import org.keycloak.operator.v2alpha1.crds.Keycloak; import org.keycloak.operator.v2alpha1.crds.Keycloak;
import java.nio.charset.StandardCharsets;
import java.time.Duration; import java.time.Duration;
import java.util.Base64;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.keycloak.operator.Constants.DEFAULT_LABELS; import static org.keycloak.operator.Constants.DEFAULT_LABELS;
import static org.keycloak.operator.utils.K8sUtils.deployKeycloak; import static org.keycloak.operator.utils.K8sUtils.deployKeycloak;
@ -246,4 +251,75 @@ public class KeycloakDeploymentE2EIT extends ClusterOperatorTest {
} }
} }
// Reference curl command:
// curl --insecure --data "grant_type=password&client_id=admin-cli&username=admin&password=adminPassword" https://localhost:8443/realms/master/protocol/openid-connect/token
@Test
public void testInitialAdminUser() {
try {
// Recreating the database to keep this test isolated
deleteDB();
deployDB();
var kc = getDefaultKeycloakDeployment();
deployKeycloak(k8sclient, kc, true);
var decoder = Base64.getDecoder();
var service = new KeycloakService(k8sclient, kc);
var kcAdminSecret = new KeycloakAdminSecret(k8sclient, kc);
AtomicReference<String> adminUsername = new AtomicReference<>();
AtomicReference<String> adminPassword = new AtomicReference<>();
Awaitility.await()
.ignoreExceptions()
.untilAsserted(() -> {
Log.info("Checking secret, ns: " + namespace + ", name: " + kcAdminSecret.getName());
var adminSecret = k8sclient
.secrets()
.inNamespace(namespace)
.withName(kcAdminSecret.getName())
.get();
adminUsername.set(new String(decoder.decode(adminSecret.getData().get("username").getBytes(StandardCharsets.UTF_8))));
adminPassword.set(new String(decoder.decode(adminSecret.getData().get("password").getBytes(StandardCharsets.UTF_8))));
String url = "https://" + service.getName() + "." + namespace + ":" + Constants.KEYCLOAK_HTTPS_PORT + "/realms/master/protocol/openid-connect/token";
Log.info("Checking url: " + url);
var curlOutput = K8sUtils.inClusterCurl(k8sclient, namespace, "--insecure", "-s", "--data", "grant_type=password&client_id=admin-cli&username=" + adminUsername.get() + "&password=" + adminPassword.get(), url);
Log.info("Curl Output: " + curlOutput);
assertTrue(curlOutput.contains("\"access_token\""));
assertTrue(curlOutput.contains("\"token_type\":\"Bearer\""));
});
// Redeploy the same Keycloak without redeploying the Database
k8sclient.resource(kc).delete();
deployKeycloak(k8sclient, kc, true);
Awaitility.await()
.ignoreExceptions()
.untilAsserted(() -> {
Log.info("Checking secret, ns: " + namespace + ", name: " + kcAdminSecret.getName());
var adminSecret = k8sclient
.secrets()
.inNamespace(namespace)
.withName(kcAdminSecret.getName())
.get();
var newPassword = new String(decoder.decode(adminSecret.getData().get("password").getBytes(StandardCharsets.UTF_8)));
String url = "https://" + service.getName() + "." + namespace + ":" + Constants.KEYCLOAK_HTTPS_PORT + "/realms/master/protocol/openid-connect/token";
Log.info("Checking url: " + url);
var curlOutput = K8sUtils.inClusterCurl(k8sclient, namespace, "--insecure", "-s", "--data", "grant_type=password&client_id=admin-cli&username=" + adminUsername.get() + "&password=" + adminPassword.get(), url);
Log.info("Curl Output: " + curlOutput);
assertTrue(curlOutput.contains("\"access_token\""));
assertTrue(curlOutput.contains("\"token_type\":\"Bearer\""));
assertNotEquals(adminPassword.get(), newPassword);
});
} catch (Exception e) {
savePodLogs();
throw e;
}
}
} }

View file

@ -46,7 +46,7 @@ public class PodTemplateTest {
spec.setHostname("example.com"); spec.setHostname("example.com");
spec.setTlsSecret("example-tls-secret"); spec.setTlsSecret("example-tls-secret");
kc.setSpec(spec); kc.setSpec(spec);
var deployment = new KeycloakDeployment(null, config, kc, new Deployment()); var deployment = new KeycloakDeployment(null, config, kc, new Deployment(), "dummy-admin");
return (Deployment) deployment.getReconciledResource().get(); return (Deployment) deployment.getReconciledResource().get();
} }