enhance: add bootstrap admin handling to the operator (#31646)
switching to manual invocation of statefulset reconciliation closes: #30004 * Update docs/guides/operator/advanced-configuration.adoc * enhance: add bootstrap admin handling to the operator closes: #30004 --------- Signed-off-by: Steve Hawkins <shawkins@redhat.com> Signed-off-by: Steven Hawkins <shawkins@redhat.com> Co-authored-by: Václav Muzikář <vaclav@muzikari.cz>
This commit is contained in:
parent
afec4401fc
commit
6a91436746
11 changed files with 202 additions and 55 deletions
|
@ -308,4 +308,10 @@ stringData:
|
|||
When running on a Kubernetes or OpenShift environment well-known locations of trusted certificates are included automatically.
|
||||
This includes `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt` and the `/var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt` when present.
|
||||
|
||||
=== Admin Bootstrapping
|
||||
|
||||
When you create a new instance the Keycloak CR spec.bootstrapAdmin stanza may be used to configure the bootstrap user and/or service account. If you do not specify anything for the spec.bootstrapAdmin, the operator will create a Secret named "metadata.name"-initial-admin with a username temp-admin and a generated password. If you specify a Secret name for bootstrap admin user, then the Secret will need to contain `username` and `password` key value pairs. If you specify a Secret name for bootstrap admin service account, then the Secret will need to contain `client-id` and `client-secret` key value pairs.
|
||||
|
||||
If a master realm has already been created for you cluster, then the spec.boostrapAdmin is effectively ignored. If you need to create a recovery admin account, then you'll need to run the CLI command against a Pod directly.
|
||||
|
||||
</@tmpl.guide>
|
||||
|
|
|
@ -5,14 +5,17 @@ import io.fabric8.kubernetes.api.model.SecretBuilder;
|
|||
import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil;
|
||||
import io.javaoperatorsdk.operator.api.reconciler.Context;
|
||||
import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
|
||||
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
|
||||
import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected;
|
||||
import io.javaoperatorsdk.operator.processing.dependent.Creator;
|
||||
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
|
||||
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource;
|
||||
import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
|
||||
|
||||
import org.keycloak.operator.Constants;
|
||||
import org.keycloak.operator.Utils;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.spec.BootstrapAdminSpec;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
@ -20,6 +23,15 @@ import java.util.UUID;
|
|||
@KubernetesDependent(labelSelector = Constants.DEFAULT_LABELS_AS_STRING, resourceDiscriminator = KeycloakAdminSecretDependentResource.NameResourceDiscriminator.class)
|
||||
public class KeycloakAdminSecretDependentResource extends KubernetesDependentResource<Secret, Keycloak> implements Creator<Secret, Keycloak>, GarbageCollected<Keycloak> {
|
||||
|
||||
public static class EnabledCondition implements Condition<Secret, Keycloak> {
|
||||
@Override
|
||||
public boolean isMet(DependentResource<Secret, Keycloak> dependentResource, Keycloak primary,
|
||||
Context<Keycloak> context) {
|
||||
return Optional.ofNullable(primary.getSpec().getBootstrapAdminSpec()).map(BootstrapAdminSpec::getUser)
|
||||
.map(BootstrapAdminSpec.User::getSecret).filter(s -> !s.equals(KeycloakAdminSecretDependentResource.getName(primary))).isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public static class NameResourceDiscriminator implements ResourceDiscriminator<Secret, Keycloak> {
|
||||
@Override
|
||||
public Optional<Secret> distinguish(Class<Secret> resource, Keycloak primary, Context<Keycloak> context) {
|
||||
|
@ -39,8 +51,9 @@ public class KeycloakAdminSecretDependentResource extends KubernetesDependentRes
|
|||
.addToLabels(Utils.allInstanceLabels(primary))
|
||||
.withNamespace(primary.getMetadata().getNamespace())
|
||||
.endMetadata()
|
||||
.withType("Opaque")
|
||||
.withType("kubernetes.io/basic-auth")
|
||||
.addToData("username", Utils.asBase64("admin"))
|
||||
.addToData("username", Utils.asBase64("temp-admin"))
|
||||
.addToData("password", Utils.asBase64(UUID.randomUUID().toString().replace("-", "")))
|
||||
.build();
|
||||
}
|
||||
|
|
|
@ -60,8 +60,7 @@ import jakarta.inject.Inject;
|
|||
|
||||
@ControllerConfiguration(
|
||||
dependents = {
|
||||
@Dependent(type = KeycloakDeploymentDependentResource.class),
|
||||
@Dependent(type = KeycloakAdminSecretDependentResource.class),
|
||||
@Dependent(type = KeycloakAdminSecretDependentResource.class, reconcilePrecondition = KeycloakAdminSecretDependentResource.EnabledCondition.class),
|
||||
@Dependent(type = KeycloakIngressDependentResource.class, reconcilePrecondition = KeycloakIngressDependentResource.EnabledCondition.class),
|
||||
@Dependent(type = KeycloakServiceDependentResource.class, useEventSourceWithName = "serviceSource"),
|
||||
@Dependent(type = KeycloakDiscoveryServiceDependentResource.class, useEventSourceWithName = "serviceSource")
|
||||
|
@ -79,6 +78,8 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
|
|||
@Inject
|
||||
KeycloakDistConfigurator distConfigurator;
|
||||
|
||||
volatile KeycloakDeploymentDependentResource deploymentDependentResource;
|
||||
|
||||
@Override
|
||||
public Map<String, EventSource> prepareEventSources(EventSourceContext<Keycloak> context) {
|
||||
var namespaces = context.getControllerConfiguration().getNamespaces();
|
||||
|
@ -94,6 +95,10 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
|
|||
|
||||
Map<String, EventSource> sources = new HashMap<>();
|
||||
sources.put("serviceSource", servicesEvent);
|
||||
|
||||
this.deploymentDependentResource = new KeycloakDeploymentDependentResource(config, watchedResources, distConfigurator);
|
||||
sources.putAll(EventSourceInitializer.nameEventSourcesFromDependentResource(context, this.deploymentDependentResource));
|
||||
|
||||
return sources;
|
||||
}
|
||||
|
||||
|
@ -104,6 +109,8 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
|
|||
|
||||
Log.debugf("--- Reconciling Keycloak: %s in namespace: %s", kcName, namespace);
|
||||
|
||||
// TODO - these modifications to the resource may belong in a webhook because dependents run first
|
||||
// only the statefulset is deferred until after
|
||||
boolean modifiedSpec = false;
|
||||
if (kc.getSpec().getInstances() == null) {
|
||||
// explicitly set defaults - and let another reconciliation happen
|
||||
|
@ -126,6 +133,9 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
|
|||
return UpdateControl.updateResource(kc);
|
||||
}
|
||||
|
||||
// after the spec has possibly been updated, reconcile the StatefulSet
|
||||
this.deploymentDependentResource.reconcile(kc, context);
|
||||
|
||||
var statusAggregator = new KeycloakStatusAggregator(kc.getStatus(), kc.getMetadata().getGeneration());
|
||||
|
||||
updateStatus(kc, context.getSecondaryResource(StatefulSet.class).orElse(null), statusAggregator, context);
|
||||
|
|
|
@ -35,7 +35,7 @@ import io.fabric8.kubernetes.api.model.apps.StatefulSetSpec;
|
|||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
import io.javaoperatorsdk.operator.api.reconciler.Context;
|
||||
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
|
||||
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
|
||||
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfigBuilder;
|
||||
import io.quarkus.logging.Log;
|
||||
|
||||
import org.keycloak.operator.Config;
|
||||
|
@ -68,13 +68,10 @@ import java.util.function.Function;
|
|||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
|
||||
import static org.keycloak.operator.Utils.addResources;
|
||||
import static org.keycloak.operator.controllers.KeycloakDistConfigurator.getKeycloakOptionEnvVarName;
|
||||
import static org.keycloak.operator.crds.v2alpha1.CRDUtils.isTlsConfigured;
|
||||
|
||||
@KubernetesDependent(labelSelector = Constants.DEFAULT_LABELS_AS_STRING)
|
||||
public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependentResource<StatefulSet, Keycloak> {
|
||||
|
||||
private static final List<String> COPY_ENV = Arrays.asList("HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY");
|
||||
|
@ -92,20 +89,23 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent
|
|||
|
||||
public static final String OPTIMIZED_ARG = "--optimized";
|
||||
|
||||
@Inject
|
||||
Config operatorConfig;
|
||||
|
||||
@Inject
|
||||
WatchedResources watchedResources;
|
||||
|
||||
@Inject
|
||||
KeycloakDistConfigurator distConfigurator;
|
||||
|
||||
private boolean useServiceCaCrt;
|
||||
|
||||
public KeycloakDeploymentDependentResource() {
|
||||
public KeycloakDeploymentDependentResource(Config operatorConfig, WatchedResources watchedResources, KeycloakDistConfigurator distConfigurator) {
|
||||
super(StatefulSet.class);
|
||||
this.operatorConfig = operatorConfig;
|
||||
this.watchedResources = watchedResources;
|
||||
this.distConfigurator = distConfigurator;
|
||||
useServiceCaCrt = Files.exists(Path.of(SERVICE_CA_CRT));
|
||||
this.configureWith(new KubernetesDependentResourceConfigBuilder<StatefulSet>()
|
||||
.withLabelSelector(Constants.DEFAULT_LABELS_AS_STRING)
|
||||
.build());
|
||||
}
|
||||
|
||||
public void setUseServiceCaCrt(boolean useServiceCaCrt) {
|
||||
|
@ -403,8 +403,7 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent
|
|||
private void addEnvVars(StatefulSet baseDeployment, Keycloak keycloakCR, TreeSet<String> allSecrets) {
|
||||
var firstClasssEnvVars = distConfigurator.configureDistOptions(keycloakCR);
|
||||
|
||||
String adminSecretName = KeycloakAdminSecretDependentResource.getName(keycloakCR);
|
||||
var additionalEnvVars = getDefaultAndAdditionalEnvVars(keycloakCR, adminSecretName);
|
||||
var additionalEnvVars = getDefaultAndAdditionalEnvVars(keycloakCR);
|
||||
|
||||
var env = Optional.ofNullable(baseDeployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv()).orElse(List.of());
|
||||
|
||||
|
@ -426,15 +425,14 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent
|
|||
|
||||
// watch the secrets used by secret key - we don't currently expect configmaps, optional refs, or watch the initial-admin
|
||||
TreeSet<String> serverConfigSecretsNames = envVars.stream().map(EnvVar::getValueFrom).filter(Objects::nonNull)
|
||||
.map(EnvVarSource::getSecretKeyRef).filter(Objects::nonNull).map(SecretKeySelector::getName)
|
||||
.filter(n -> !n.equals(adminSecretName)).collect(Collectors.toCollection(TreeSet::new));
|
||||
.map(EnvVarSource::getSecretKeyRef).filter(Objects::nonNull).map(SecretKeySelector::getName).collect(Collectors.toCollection(TreeSet::new));
|
||||
|
||||
Log.debugf("Found config secrets names: %s", serverConfigSecretsNames);
|
||||
|
||||
allSecrets.addAll(serverConfigSecretsNames);
|
||||
}
|
||||
|
||||
private List<EnvVar> getDefaultAndAdditionalEnvVars(Keycloak keycloakCR, String adminSecretName) {
|
||||
private List<EnvVar> getDefaultAndAdditionalEnvVars(Keycloak keycloakCR) {
|
||||
// default config values
|
||||
List<ValueOrSecret> serverConfigsList = new ArrayList<>(Constants.DEFAULT_DIST_CONFIG_LIST);
|
||||
|
||||
|
@ -467,29 +465,6 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent
|
|||
}
|
||||
}
|
||||
|
||||
envVars.add(
|
||||
new EnvVarBuilder()
|
||||
.withName("KEYCLOAK_ADMIN")
|
||||
.withNewValueFrom()
|
||||
.withNewSecretKeyRef()
|
||||
.withName(adminSecretName)
|
||||
.withKey("username")
|
||||
.withOptional(false)
|
||||
.endSecretKeyRef()
|
||||
.endValueFrom()
|
||||
.build());
|
||||
envVars.add(
|
||||
new EnvVarBuilder()
|
||||
.withName("KEYCLOAK_ADMIN_PASSWORD")
|
||||
.withNewValueFrom()
|
||||
.withNewSecretKeyRef()
|
||||
.withName(adminSecretName)
|
||||
.withKey("password")
|
||||
.withOptional(false)
|
||||
.endSecretKeyRef()
|
||||
.endValueFrom()
|
||||
.build());
|
||||
|
||||
return envVars;
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ import org.keycloak.operator.Constants;
|
|||
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusAggregator;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.spec.BootstrapAdminSpec;
|
||||
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.HostnameSpec;
|
||||
|
@ -73,6 +74,7 @@ public class KeycloakDistConfigurator {
|
|||
configureCache();
|
||||
configureProxy();
|
||||
configureManagement();
|
||||
configureBootstrapAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -86,6 +88,26 @@ public class KeycloakDistConfigurator {
|
|||
|
||||
/* ---------- Configuration of first-class citizen fields ---------- */
|
||||
|
||||
void configureBootstrapAdmin() {
|
||||
optionMapper(Function.identity())
|
||||
.mapOption("bootstrap-admin-username",
|
||||
keycloakCR -> Optional.ofNullable(keycloakCR.getSpec().getBootstrapAdminSpec())
|
||||
.map(BootstrapAdminSpec::getUser).map(BootstrapAdminSpec.User::getSecret)
|
||||
.or(() -> Optional.of(KeycloakAdminSecretDependentResource.getName(keycloakCR)))
|
||||
.map(s -> new SecretKeySelector("username", s, null)).orElse(null))
|
||||
.mapOption("bootstrap-admin-password",
|
||||
keycloakCR -> Optional.ofNullable(keycloakCR.getSpec().getBootstrapAdminSpec())
|
||||
.map(BootstrapAdminSpec::getUser).map(BootstrapAdminSpec.User::getSecret)
|
||||
.or(() -> Optional.of(KeycloakAdminSecretDependentResource.getName(keycloakCR)))
|
||||
.map(s -> new SecretKeySelector("password", s, null)).orElse(null));
|
||||
|
||||
optionMapper(keycloakCR -> keycloakCR.getSpec().getBootstrapAdminSpec())
|
||||
.mapOption("bootstrap-admin-client-id",
|
||||
spec -> Optional.ofNullable(spec.getService()).map(BootstrapAdminSpec.Service::getSecret).map(s -> new SecretKeySelector("client-id", s, null)).orElse(null))
|
||||
.mapOption("bootstrap-admin-client-secret",
|
||||
spec -> Optional.ofNullable(spec.getService()).map(BootstrapAdminSpec.Service::getSecret).map(s -> new SecretKeySelector("client-secret", s, null)).orElse(null));
|
||||
}
|
||||
|
||||
void configureHostname() {
|
||||
optionMapper(keycloakCR -> keycloakCR.getSpec().getHostnameSpec())
|
||||
.mapOption("hostname", HostnameSpec::getHostname)
|
||||
|
|
|
@ -20,6 +20,7 @@ import io.fabric8.kubernetes.api.model.LocalObjectReference;
|
|||
import io.fabric8.kubernetes.api.model.ResourceRequirements;
|
||||
import io.fabric8.kubernetes.model.annotation.SpecReplicas;
|
||||
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.spec.BootstrapAdminSpec;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.spec.CacheSpec;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.spec.DatabaseSpec;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.spec.FeatureSpec;
|
||||
|
@ -115,6 +116,10 @@ public class KeycloakSpec {
|
|||
@JsonPropertyDescription("In this section you can configure Keycloak's scheduling")
|
||||
private SchedulingSpec schedulingSpec;
|
||||
|
||||
@JsonProperty("bootstrapAdmin")
|
||||
@JsonPropertyDescription("In this section you can configure Keycloak's bootstrap admin - will be used only for inital cluster creation.")
|
||||
private BootstrapAdminSpec bootstrapAdminSpec;
|
||||
|
||||
public HttpSpec getHttpSpec() {
|
||||
return httpSpec;
|
||||
}
|
||||
|
@ -264,4 +269,12 @@ public class KeycloakSpec {
|
|||
public void setSchedulingSpec(SchedulingSpec schedulingSpec) {
|
||||
this.schedulingSpec = schedulingSpec;
|
||||
}
|
||||
|
||||
public BootstrapAdminSpec getBootstrapAdminSpec() {
|
||||
return bootstrapAdminSpec;
|
||||
}
|
||||
|
||||
public void setBootstrapAdminSpec(BootstrapAdminSpec bootstrapAdminSpec) {
|
||||
this.bootstrapAdminSpec = bootstrapAdminSpec;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package org.keycloak.operator.crds.v2alpha1.deployment.spec;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
|
||||
|
||||
import io.sundr.builder.annotations.Buildable;
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@Buildable(editableEnabled = false, builderPackage = "io.fabric8.kubernetes.api.builder")
|
||||
public class BootstrapAdminSpec {
|
||||
|
||||
public static class User {
|
||||
@JsonPropertyDescription("Name of the Secret that contains the username and password keys")
|
||||
private String secret;
|
||||
|
||||
public String getSecret() {
|
||||
return secret;
|
||||
}
|
||||
|
||||
public void setSecret(String secret) {
|
||||
this.secret = secret;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Service {
|
||||
@JsonPropertyDescription("Name of the Secret that contains the client-id and client-secret keys")
|
||||
private String secret;
|
||||
|
||||
public String getSecret() {
|
||||
return secret;
|
||||
}
|
||||
|
||||
public void setSecret(String secret) {
|
||||
this.secret = secret;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//private Integer expiration;
|
||||
@JsonPropertyDescription("Configures the bootstrap admin user")
|
||||
private User user;
|
||||
@JsonPropertyDescription("Configures the bootstrap admin service account")
|
||||
private Service service;
|
||||
|
||||
/*public Integer getExpiration() {
|
||||
return expiration;
|
||||
}
|
||||
|
||||
public void setExpiration(Integer expiration) {
|
||||
this.expiration = expiration;
|
||||
}*/
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public void setUser(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
public Service getService() {
|
||||
return service;
|
||||
}
|
||||
|
||||
public void setService(Service service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
}
|
|
@ -46,6 +46,7 @@ import org.keycloak.operator.controllers.KeycloakServiceDependentResource;
|
|||
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusCondition;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.spec.BootstrapAdminSpec;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.spec.HostnameSpecBuilder;
|
||||
import org.keycloak.operator.testsuite.unit.WatchedResourcesTest;
|
||||
import org.keycloak.operator.testsuite.utils.CRAssert;
|
||||
|
@ -68,7 +69,6 @@ import static java.util.concurrent.TimeUnit.MINUTES;
|
|||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
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.testsuite.utils.CRAssert.assertKeycloakStatusCondition;
|
||||
import static org.keycloak.operator.testsuite.utils.K8sUtils.deployKeycloak;
|
||||
|
@ -384,22 +384,29 @@ public class KeycloakDeploymentTest extends BaseOperatorTest {
|
|||
assertKeycloakAccessibleViaService(kc, false, httpPort);
|
||||
}
|
||||
|
||||
// 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() {
|
||||
var kc = getTestKeycloakDeployment(true);
|
||||
String secretName = KeycloakAdminSecretDependentResource.getName(kc);
|
||||
assertInitialAdminUser(secretName, kc, false);
|
||||
}
|
||||
|
||||
k8sclient
|
||||
.resources(Keycloak.class)
|
||||
.inNamespace(namespace)
|
||||
.delete();
|
||||
k8sclient
|
||||
.secrets()
|
||||
.inNamespace(namespace)
|
||||
.withName(secretName)
|
||||
.delete();
|
||||
@Test
|
||||
public void testCustomBootstrapAdminUser() {
|
||||
var kc = getTestKeycloakDeployment(true);
|
||||
String secretName = "my-secret";
|
||||
// fluents don't seem to work here because of the inner classes
|
||||
kc.getSpec().setBootstrapAdminSpec(new BootstrapAdminSpec());
|
||||
kc.getSpec().getBootstrapAdminSpec().setUser(new BootstrapAdminSpec.User());
|
||||
kc.getSpec().getBootstrapAdminSpec().getUser().setSecret(secretName);
|
||||
k8sclient.resource(new SecretBuilder().withNewMetadata().withName(secretName).endMetadata()
|
||||
.addToStringData("username", "user").addToStringData("password", "pass20rd").build()).create();
|
||||
assertInitialAdminUser(secretName, kc, true);
|
||||
}
|
||||
|
||||
// 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
|
||||
public void assertInitialAdminUser(String secretName, Keycloak kc, boolean samePasswordAfterReinstall) {
|
||||
|
||||
// Making sure no other Keycloak pod is still around
|
||||
Awaitility.await()
|
||||
|
@ -422,6 +429,7 @@ public class KeycloakDeploymentTest extends BaseOperatorTest {
|
|||
AtomicReference<String> adminPassword = new AtomicReference<>();
|
||||
Awaitility.await()
|
||||
.ignoreExceptions()
|
||||
.atMost(3, TimeUnit.MINUTES)
|
||||
.untilAsserted(() -> {
|
||||
Log.info("Checking secret, ns: " + namespace + ", name: " + secretName);
|
||||
var adminSecret = k8sclient
|
||||
|
@ -443,11 +451,12 @@ public class KeycloakDeploymentTest extends BaseOperatorTest {
|
|||
assertTrue(curlOutput.contains("\"token_type\":\"Bearer\""));
|
||||
});
|
||||
|
||||
// Redeploy the same Keycloak without redeploying the Database
|
||||
// Redeploy the same Keycloak without redeploying the Database - the secret may change, but the admin password does not
|
||||
k8sclient.resource(kc).delete();
|
||||
deployKeycloak(k8sclient, kc, true);
|
||||
Awaitility.await()
|
||||
.ignoreExceptions()
|
||||
.atMost(3, TimeUnit.MINUTES)
|
||||
.untilAsserted(() -> {
|
||||
Log.info("Checking secret, ns: " + namespace + ", name: " + secretName);
|
||||
var adminSecret = k8sclient
|
||||
|
@ -466,7 +475,7 @@ public class KeycloakDeploymentTest extends BaseOperatorTest {
|
|||
|
||||
assertTrue(curlOutput.contains("\"access_token\""));
|
||||
assertTrue(curlOutput.contains("\"token_type\":\"Bearer\""));
|
||||
assertNotEquals(adminPassword.get(), newPassword);
|
||||
assertEquals(samePasswordAfterReinstall, adminPassword.get().equals(newPassword));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -155,6 +155,18 @@ public class KeycloakDistConfiguratorTest {
|
|||
testFirstClassCitizen(expectedValues);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bootstrapAdmin() {
|
||||
final Map<String, String> expectedValues = Map.of(
|
||||
"bootstrap-admin-username", "something",
|
||||
"bootstrap-admin-password", "something",
|
||||
"bootstrap-admin-client-id", "else",
|
||||
"bootstrap-admin-client-secret", "else"
|
||||
);
|
||||
|
||||
testFirstClassCitizen(expectedValues);
|
||||
}
|
||||
|
||||
/* UTILS */
|
||||
|
||||
private void testFirstClassCitizen(Map<String, String> expectedValues) {
|
||||
|
|
|
@ -38,10 +38,13 @@ import io.quarkus.test.InjectMock;
|
|||
import io.quarkus.test.junit.QuarkusTest;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.operator.Config;
|
||||
import org.keycloak.operator.Constants;
|
||||
import org.keycloak.operator.Utils;
|
||||
import org.keycloak.operator.controllers.KeycloakDeploymentDependentResource;
|
||||
import org.keycloak.operator.controllers.KeycloakDistConfigurator;
|
||||
import org.keycloak.operator.controllers.WatchedResources;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakBuilder;
|
||||
|
@ -72,8 +75,18 @@ public class PodTemplateTest {
|
|||
WatchedResources watchedResources;
|
||||
|
||||
@Inject
|
||||
Config operatorConfig;
|
||||
|
||||
@Inject
|
||||
KeycloakDistConfigurator distConfigurator;
|
||||
|
||||
KeycloakDeploymentDependentResource deployment;
|
||||
|
||||
@BeforeEach
|
||||
protected void setup() {
|
||||
this.deployment = new KeycloakDeploymentDependentResource(operatorConfig, watchedResources, distConfigurator);
|
||||
}
|
||||
|
||||
private StatefulSet getDeployment(PodTemplateSpec podTemplate, StatefulSet existingDeployment, Consumer<KeycloakSpecBuilder> additionalSpec) {
|
||||
var kc = new KeycloakBuilder().withNewMetadata().withName("instance").endMetadata().build();
|
||||
existingDeployment = new StatefulSetBuilder(existingDeployment).editOrNewSpec().editOrNewSelector()
|
||||
|
|
|
@ -72,6 +72,11 @@ spec:
|
|||
name: my-secret
|
||||
httpManagement:
|
||||
port: 9003
|
||||
bootstrapAdmin:
|
||||
user:
|
||||
secret: something
|
||||
service:
|
||||
secret: else
|
||||
unsupported:
|
||||
podTemplate:
|
||||
metadata:
|
||||
|
|
Loading…
Reference in a new issue