Revert "operator bootstrap admin handling (#30711)"
This reverts commit 3139b82e3c
.
This commit is contained in:
parent
bcbff87336
commit
6f62e6768c
10 changed files with 46 additions and 173 deletions
|
@ -308,10 +308,4 @@ 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>
|
||||
|
|
|
@ -2,21 +2,17 @@ package org.keycloak.operator.controllers;
|
|||
|
||||
import io.fabric8.kubernetes.api.model.Secret;
|
||||
import io.fabric8.kubernetes.api.model.SecretBuilder;
|
||||
import io.fabric8.kubernetes.api.model.networking.v1.Ingress;
|
||||
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;
|
||||
|
@ -24,15 +20,6 @@ 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<Ingress, Keycloak> {
|
||||
@Override
|
||||
public boolean isMet(DependentResource<Ingress, 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) {
|
||||
|
@ -52,9 +39,8 @@ 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("temp-admin"))
|
||||
.addToData("username", Utils.asBase64("admin"))
|
||||
.addToData("password", Utils.asBase64(UUID.randomUUID().toString().replace("-", "")))
|
||||
.build();
|
||||
}
|
||||
|
|
|
@ -47,7 +47,6 @@ import org.keycloak.operator.Utils;
|
|||
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatus;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusAggregator;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.spec.BootstrapAdminSpec;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.spec.HostnameSpec;
|
||||
import org.keycloak.operator.crds.v2alpha1.deployment.spec.HostnameSpecBuilder;
|
||||
|
||||
|
@ -62,7 +61,7 @@ import jakarta.inject.Inject;
|
|||
@ControllerConfiguration(
|
||||
dependents = {
|
||||
@Dependent(type = KeycloakDeploymentDependentResource.class),
|
||||
@Dependent(type = KeycloakAdminSecretDependentResource.class, reconcilePrecondition = KeycloakAdminSecretDependentResource.EnabledCondition.class),
|
||||
@Dependent(type = KeycloakAdminSecretDependentResource.class),
|
||||
@Dependent(type = KeycloakIngressDependentResource.class, reconcilePrecondition = KeycloakIngressDependentResource.EnabledCondition.class),
|
||||
@Dependent(type = KeycloakServiceDependentResource.class, useEventSourceWithName = "serviceSource"),
|
||||
@Dependent(type = KeycloakDiscoveryServiceDependentResource.class, useEventSourceWithName = "serviceSource")
|
||||
|
@ -105,7 +104,6 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
|
|||
|
||||
Log.debugf("--- Reconciling Keycloak: %s in namespace: %s", kcName, namespace);
|
||||
|
||||
// TODO - these modifications to the resource belong in a webhook because dependents run first
|
||||
boolean modifiedSpec = false;
|
||||
if (kc.getSpec().getInstances() == null) {
|
||||
// explicitly set defaults - and let another reconciliation happen
|
||||
|
|
|
@ -403,7 +403,8 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent
|
|||
private void addEnvVars(StatefulSet baseDeployment, Keycloak keycloakCR, TreeSet<String> allSecrets) {
|
||||
var firstClasssEnvVars = distConfigurator.configureDistOptions(keycloakCR);
|
||||
|
||||
var additionalEnvVars = getDefaultAndAdditionalEnvVars(keycloakCR);
|
||||
String adminSecretName = KeycloakAdminSecretDependentResource.getName(keycloakCR);
|
||||
var additionalEnvVars = getDefaultAndAdditionalEnvVars(keycloakCR, adminSecretName);
|
||||
|
||||
var env = Optional.ofNullable(baseDeployment.getSpec().getTemplate().getSpec().getContainers().get(0).getEnv()).orElse(List.of());
|
||||
|
||||
|
@ -425,14 +426,15 @@ 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).collect(Collectors.toCollection(TreeSet::new));
|
||||
.map(EnvVarSource::getSecretKeyRef).filter(Objects::nonNull).map(SecretKeySelector::getName)
|
||||
.filter(n -> !n.equals(adminSecretName)).collect(Collectors.toCollection(TreeSet::new));
|
||||
|
||||
Log.debugf("Found config secrets names: %s", serverConfigSecretsNames);
|
||||
|
||||
allSecrets.addAll(serverConfigSecretsNames);
|
||||
}
|
||||
|
||||
private List<EnvVar> getDefaultAndAdditionalEnvVars(Keycloak keycloakCR) {
|
||||
private List<EnvVar> getDefaultAndAdditionalEnvVars(Keycloak keycloakCR, String adminSecretName) {
|
||||
// default config values
|
||||
List<ValueOrSecret> serverConfigsList = new ArrayList<>(Constants.DEFAULT_DIST_CONFIG_LIST);
|
||||
|
||||
|
@ -465,6 +467,29 @@ 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,7 +28,6 @@ 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;
|
||||
|
@ -74,7 +73,6 @@ public class KeycloakDistConfigurator {
|
|||
configureCache();
|
||||
configureProxy();
|
||||
configureManagement();
|
||||
configureBootstrapAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -88,26 +86,6 @@ 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,7 +20,6 @@ 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;
|
||||
|
@ -116,10 +115,6 @@ 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;
|
||||
}
|
||||
|
@ -269,12 +264,4 @@ public class KeycloakSpec {
|
|||
public void setSchedulingSpec(SchedulingSpec schedulingSpec) {
|
||||
this.schedulingSpec = schedulingSpec;
|
||||
}
|
||||
|
||||
public BootstrapAdminSpec getBootstrapAdminSpec() {
|
||||
return bootstrapAdminSpec;
|
||||
}
|
||||
|
||||
public void setBootstrapAdminSpec(BootstrapAdminSpec bootstrapAdminSpec) {
|
||||
this.bootstrapAdminSpec = bootstrapAdminSpec;
|
||||
}
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
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,7 +46,6 @@ 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;
|
||||
|
@ -69,6 +68,7 @@ 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,29 +384,22 @@ 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);
|
||||
}
|
||||
|
||||
@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) {
|
||||
k8sclient
|
||||
.resources(Keycloak.class)
|
||||
.inNamespace(namespace)
|
||||
.delete();
|
||||
k8sclient
|
||||
.secrets()
|
||||
.inNamespace(namespace)
|
||||
.withName(secretName)
|
||||
.delete();
|
||||
|
||||
// Making sure no other Keycloak pod is still around
|
||||
Awaitility.await()
|
||||
|
@ -429,7 +422,6 @@ 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
|
||||
|
@ -451,12 +443,11 @@ public class KeycloakDeploymentTest extends BaseOperatorTest {
|
|||
assertTrue(curlOutput.contains("\"token_type\":\"Bearer\""));
|
||||
});
|
||||
|
||||
// Redeploy the same Keycloak without redeploying the Database - the secret may change, but the admin password does not
|
||||
// Redeploy the same Keycloak without redeploying the Database
|
||||
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
|
||||
|
@ -475,7 +466,7 @@ public class KeycloakDeploymentTest extends BaseOperatorTest {
|
|||
|
||||
assertTrue(curlOutput.contains("\"access_token\""));
|
||||
assertTrue(curlOutput.contains("\"token_type\":\"Bearer\""));
|
||||
assertEquals(samePasswordAfterReinstall, adminPassword.get().equals(newPassword));
|
||||
assertNotEquals(adminPassword.get(), newPassword);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -155,18 +155,6 @@ 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) {
|
||||
|
|
|
@ -72,11 +72,6 @@ spec:
|
|||
name: my-secret
|
||||
httpManagement:
|
||||
port: 9003
|
||||
bootstrapAdmin:
|
||||
user:
|
||||
secret: something
|
||||
service:
|
||||
secret: else
|
||||
unsupported:
|
||||
podTemplate:
|
||||
metadata:
|
||||
|
|
Loading…
Reference in a new issue