Initial bootstrap admin
This commit is contained in:
parent
6621fb3988
commit
59450948f4
6 changed files with 162 additions and 4 deletions
|
@ -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; }
|
||||
|
||||
}
|
|
@ -91,9 +91,12 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
|
|||
|
||||
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
|
||||
// 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.createOrUpdateReconciled();
|
||||
|
||||
|
|
|
@ -54,11 +54,13 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu
|
|||
private final Keycloak keycloakCR;
|
||||
private final Deployment existingDeployment;
|
||||
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);
|
||||
this.config = config;
|
||||
this.keycloakCR = keycloakCR;
|
||||
this.adminSecretName = adminSecretName;
|
||||
|
||||
if (existingDeployment != null) {
|
||||
Log.info("Existing Deployment provided by controller");
|
||||
|
@ -483,12 +485,37 @@ public class KeycloakDeployment extends OperatorManagedResource implements Statu
|
|||
if (keycloakCR.getSpec().getServerConfiguration() != null) {
|
||||
serverConfig.putAll(keycloakCR.getSpec().getServerConfiguration());
|
||||
}
|
||||
return serverConfig.entrySet().stream()
|
||||
var envVars = serverConfig.entrySet().stream()
|
||||
.map(e -> new EnvVarBuilder()
|
||||
.withName(e.getKey())
|
||||
.withValue(e.getValue())
|
||||
.build())
|
||||
.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) {
|
||||
|
|
|
@ -163,6 +163,11 @@ public abstract class ClusterOperatorTest {
|
|||
.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)
|
||||
protected void savePodLogs() {
|
||||
Log.infof("Saving pod logs to %s", POD_LOGS_DIR);
|
||||
|
|
|
@ -7,16 +7,21 @@ import io.quarkus.test.junit.QuarkusTest;
|
|||
import org.awaitility.Awaitility;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.operator.utils.K8sUtils;
|
||||
import org.keycloak.operator.v2alpha1.KeycloakAdminSecret;
|
||||
import org.keycloak.operator.v2alpha1.KeycloakService;
|
||||
import org.keycloak.operator.v2alpha1.crds.Keycloak;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
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.keycloak.operator.Constants.DEFAULT_LABELS;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ public class PodTemplateTest {
|
|||
spec.setHostname("example.com");
|
||||
spec.setTlsSecret("example-tls-secret");
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue