8000 finishes the conversion away from createOrReplace by shawkins · Pull Request #20933 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/documentation/release_notes/topics/22_0_0.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ In relation to the KeyStore Vault news, we also integrated Quarkus's recently re

The are additional fields available in the keycloak.status to facilitate keycloak being a scalable resource. There are also additional fields that make the status easier to interpret such as observedGeneration and condition observedGeneration and lastTransitionTime fields. However the condition status field was also changed from a boolean to a string for conformance with standard Kubernetes conditions. Please make sure any of your usage of this field is updated to expect the values "True", "False", or "Unknown", rather than true or false.

= Co-management of Operator Resources

In scenarios where advanced management is needed you may now directly update most fields on operator managed resources that have not been set by the operator directly. This can be used as an alternative to the unsupported stanza of the Keycloak spec. Like the unsupported stanza these direct modifications are not considered supported. If your modifications prevent the operator from being able to manage the resource, there Keycloak CR will show this error condition and the operator will log it.

= Account Console v3 promoted to preview

In version 21.1.0 of Keycloak the new Account Console (version 3) was introduced as an experimental feature. Starting this version it has been promoted to a preview feature.
Expand Down
6 changes: 6 additions & 0 deletions operator/src/main/java/org/keycloak/operator/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@

import io.fabric8.kubernetes.client.KubernetesClient;

import java.nio.charset.StandardCharsets;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Base64;

/**
* @author Vaclav Muzikar <vmuzikar@redhat.com>
Expand All @@ -39,4 +41,8 @@ public static String iso8601Now() {
return ZonedDateTime.now(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT);
}

public static String asBase64(String toEncode) {
return Base64.getEncoder().encodeToString(toEncode.getBytes(StandardCharsets.UTF_8));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import io.fabric8.kubernetes.api.model.SecretBuilder;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil;

import org.keycloak.operator.Utils;
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;

import java.util.Optional;
Expand Down Expand Up @@ -35,8 +37,8 @@ private Secret createSecret() {
.withNamespace(getNamespace())
.endMetadata()
.withType("kubernetes.io/basic-auth")
.addToStringData("username", "admin")
.addToStringData("password", UUID.randomUUID().toString().replace("-", ""))
.addToData("username", Utils.asBase64("admin"))
.addToData("password", Utils.asBase64(UUID.randomUUID().toString().replace("-", "")))
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,20 +64,23 @@ public Map<String, EventSource> prepareEventSources(EventSourceContext<Keycloak>
.withLabelSelector(Constants.DEFAULT_LABELS_AS_STRING)
.withNamespaces(namespace)
.withSecondaryToPrimaryMapper(Mappers.fromOwnerReference())
.withOnUpdateFilter(new MetadataAwareOnUpdateFilter<>())
.build();

InformerConfiguration<Service> servicesIC = InformerConfiguration
.from(Service.class)
.withLabelSelector(Constants.DEFAULT_LABELS_AS_STRING)
.withNamespaces(namespace)
.withSecondaryToPrimaryMapper(Mappers.fromOwnerReference())
.withOnUpdateFilter(new MetadataAwareOnUpdateFilter<>())
.build();

InformerConfiguration<Ingress> ingressesIC = InformerConfiguration
.from(Ingress.class)
.withLabelSelector(Constants.DEFAULT_LABELS_AS_STRING)
.withNamespaces(namespace)
.withSecondaryToPrimaryMapper(Mappers.fromOwnerReference())
.withOnUpdateFilter(new MetadataAwareOnUpdateFilter<>())
.build();

EventSource statefulSetEvent = new InformerEventSource<>(statefulSetIC, context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.quarkus.logging.Log;

import org.keycloak.common.util.CollectionUtil;
import org.keycloak.operator.Config;
import org.keycloak.operator.Constants;
Expand Down Expand Up @@ -86,32 +87,15 @@ public KeycloakDeployment(KubernetesClient client, Config config, Keycloak keycl

@Override
public Optional<HasMetadata> getReconciledResource() {
StatefulSet baseDeployment = new StatefulSetBuilder(this.baseDeployment).build(); // clone not to change the base template
StatefulSet reconciledDeployment;
if (existingDeployment == null) {
Log.info("No existing Deployment found, using the default");
reconciledDeployment = baseDeployment;
}
else {
Log.info("Existing Deployment found, updating specs");
reconciledDeployment = new StatefulSetBuilder(existingDeployment).build();

// don't overwrite metadata, just specs
reconciledDeployment.setSpec(baseDeployment.getSpec());

// don't fully overwrite annotations in pod templates to support rolling restarts (K8s sets some extra annotation to track restart)
// instead, merge it
if (existingDeployment.getSpec() != null && existingDeployment.getSpec().getTemplate() != null) {
mergeMaps(
Optional.ofNullable(existingDeployment.getSpec().getTemplate().getMetadata()).map(m -> m.getAnnotations()).orElse(null),
Optional.ofNullable(reconciledDeployment.getSpec().getTemplate().getMetadata()).map(m -> m.getAnnotations()).orElse(null),
annotations -> reconciledDeployment.getSpec().getTemplate().getMetadata().setAnnotations(annotations));
}
Log.info("Existing Deployment found, handling migration");

migrateDeployment(existingDeployment, reconciledDeployment);
migrateDeployment(existingDeployment, baseDeployment);
}

return Optional.of(reconciledDeployment);
return Optional.of(baseDeployment);
}

private StatefulSet fetchExistingDeployment() {
Expand Down Expand Up @@ -442,7 +426,8 @@ private List<EnvVar> getEnvVars() {

// merge with the CR; the values in CR take precedence
if (keycloakCR.getSpec().getAdditionalOptions() != null) {
serverConfigsList.removeAll(keycloakCR.getSpec().getAdditionalOptions());
Set<String> inCr = keycloakCR.getSpec().getAdditionalOptions().stream().map(v -> v.getName()).collect(Collectors.toSet());
serverConfigsList.removeIf(v -> inCr.contains(v.getName()));
serverConfigsList.addAll(keycloakCR.getSpec().getAdditionalOptions());
}

Expand Down Expand Up @@ -495,7 +480,8 @@ private List<EnvVar> getEnvVars() {

return envVars;
}


@Override
public void updateStatus(KeycloakStatusAggregator status) {
status.apply(b -> b.withSelector(Constants.DEFAULT_LABELS_AS_STRING));
validatePodTemplate(status);
Expand All @@ -512,7 +498,7 @@ public void updateStatus(KeycloakStatusAggregator status) {
status.addNotReadyMessage("Waiting for more replicas");
}
}

if (migrationInProgress) {
status.addNotReadyMessage("Performing Keycloak upgrade, scaling down the deployment");
} else if (existingDeployment.getStatus() != null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public KeycloakDiscoveryService(KubernetesClient client, Keycloak keycloakCR) {
private ServiceSpec getServiceSpec() {
return new ServiceSpecBuilder()
.addNewPort()
.withProtocol("TCP")
.withPort(Constants.KEYCLOAK_DISCOVERY_SERVICE_PORT)
.endPort()
.withSelector(Constants.DEFAULT_LABELS)
Expand All @@ -49,14 +50,7 @@ private ServiceSpec getServiceSpec() {

@Override
protected Optional<HasMetadata> getReconciledResource() {
var service = fetchExistingService();
if (service == null) {
service = newService();
} else {
service.setSpec(getServiceSpec());
}

return Optional.of(service);
return Optional.of(newService());
}

private Service newService() {
Expand All @@ -78,6 +72,7 @@ private Service fetchExistingService() {
.get();
}

@Override
public void updateStatus(KeycloakStatusAggregator status) {
if (existingService == null) {
status.addNotReadyMessage("No existing Discovery Service found, waiting for creating a new one");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,8 +252,10 @@ public <R> OptionMapper<T> mapOption(String optionName, Function<T, R> optionVal
envVarBuilder.withValue(String.valueOf(value));
}


envVars.add(envVarBuilder.build());
var toAdd = envVarBuilder.build();
if (!envVars.stream().anyMatch(envVar -> envVar.getName().equals(toAdd.getName()))) {
envVars.add(toAdd);
}

return this;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,12 @@ public KeycloakIngress(KubernetesClient client, Keycloak keycloakCR) {
protected Optional<HasMetadata> getReconciledResource() {
IngressSpec ingressSpec = keycloak.getSpec().getIngressSpec();
if (ingressSpec != null && !ingressSpec.isIngressEnabled()) {
if (existingIngress != null && isExistingIngressFromSameOwnerReference()) {
if (existingIngress != null && existingIngress.hasOwnerReferenceFor(keycloak)) {
deleteExistingIngress();
}
return Optional.empty();
} else {
var defaultIngress = newIngress();
var resultIngress = (existingIngress != null) ? existingIngress : defaultIngress;

resultIngress.getMetadata().setAnnotations(defaultIngress.getMetadata().getAnnotations());
resultIngress.setSpec(defaultIngress.getSpec());
return Optional.of(resultIngress);
return Optional.of(newIngress());
}
}

Expand Down Expand Up @@ -88,6 +83,7 @@ private Ingress newIngress() {
.withName(keycloak.getMetadata().getName() + Constants.KEYCLOAK_SERVICE_SUFFIX)
.withNewPort()
.withNumber(port)
.withName("") // for SSA to clear the name if already set
.endPort()
.endService()
.endDefaultBackend()
Expand All @@ -101,6 +97,7 @@ private Ingress newIngress() {
.withName(keycloak.getMetadata().getName() + Constants.KEYCLOAK_SERVICE_SUFFIX)
.withNewPort()
.withNumber(port)
.withName("") // for SSA to clear the name if already set
.endPort()
.endService()
.endBackend()
Expand All @@ -122,16 +119,6 @@ protected void deleteExistingIngress() {
client.resource(existingIngress).delete();
}

private boolean isExistingIngressFromSameOwnerReference() {

return existingIngress
.getMetadata()
.getOwnerReferences()
.stream()
.anyMatch(oneOwnerRef -> oneOwnerRef.getUid().equalsIgnoreCase(keycloak.getMetadata().getUid()));

}

protected Ingress fetchExistingIngress() {
return client
.network()
Expand All @@ -142,6 +129,7 @@ protected Ingress fetchExistingIngress() {
.get();
}

@Override
public void updateStatus(KeycloakStatusAggregator status) {
IngressSpec ingressSpec = keycloak.getSpec().getIngressSpec();
if (ingressSpec == null) {
Expand All @@ -153,6 +141,7 @@ public void updateStatus(KeycloakStatusAggregator status) {
}
}

@Override
public String getName() {
return cr.getMetadata().getName() + Constants.KEYCLOAK_INGRESS_SUFFIX;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,14 @@ public Map<String, EventSource> prepareEventSources(EventSourceContext<KeycloakR
.withLabelSelector(Constants.DEFAULT_LABELS_AS_STRING)
.withNamespaces(context.getControllerConfiguration().getConfigurationService().getClientConfiguration().getNamespace())
.withSecondaryToPrimaryMapper(Mappers.fromOwnerReference())
.withOnUpdateFilter(new MetadataAwareOnUpdateFilter<>())
.build();

return EventSourceInitializer.nameEventSources(new InformerEventSource<>(jobIC, context));
}

@Override
public UpdateControl<KeycloakRealmImport> reconcile(KeycloakRealmImport realm, Context context) {
public UpdateControl<KeycloakRealmImport> reconcile(KeycloakRealmImport realm, Context<KeycloakRealmImport> context) {
String realmName = realm.getMetadata().getName();
String realmNamespace = realm.getMetadata().getNamespace();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@
import io.fabric8.kubernetes.api.model.batch.v1.Job;
import io.fabric8.kubernetes.api.model.batch.v1.JobBuilder;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.ResourceNotFoundException;
import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil;
import io.quarkus.logging.Log;

import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
import org.keycloak.operator.crds.v2alpha1.realmimport.KeycloakRealmImport;
import org.keycloak.operator.crds.v2alpha1.realmimport.KeycloakRealmImportStatusBuilder;
Expand All @@ -55,15 +55,15 @@ public KeycloakRealmImportJob(KubernetesClient client, KeycloakRealmImport realm
this.secretName = secretName;
this.volumeName = KubernetesResourceUtil.sanitizeName(secretName + "-volume");

this.existingJob = fetchExistingJob();
this.existingDeployment = fetchExistingDeployment();
this.keycloak = fetchExistingKeycloak();
this.existingJob = fetchExisting(Job.class, getName());
this.existingDeployment = fetchExisting(StatefulSet.class, getKeycloakName());
this.keycloak = fetchExisting(Keycloak.class, getKeycloakName());
}

@Override
protected Optional<HasMetadata> getReconciledResource() {
if (existingDeployment == null) {
throw new ResourceNotFoundException("Keycloak Deployment not found: " + getKeycloakName());
return Optional.empty(); // handled in the status
} else if (existingJob == null) {
Log.info("Creating a new Job");
return Optional.of(createImportJob());
Expand All @@ -73,30 +73,11 @@ protected Optional<HasMetadata> getReconciledResource() {
}
}

private Job fetchExistingJob() {
return client
.batch()
.v1()
.jobs()
.inNamespace(getNamespace())
.withName(getName())
.get();
}

private StatefulSet fetchExistingDeployment() {
private <T extends HasMetadata> T fetchExisting(Class<T> type, String name) {
return client
.apps()
.statefulSets()
.resources(type)
.inNamespace(getNamespace())
.withName(getKeycloakName())
.get();
}

private Keycloak fetchExistingKeycloak() {
return client
.resources(Keycloak.class)
.inNamespace(getNamespace())
.withName(getKeycloakName())
.withName(name)
.get();
}

Expand Down Expand Up @@ -187,7 +168,7 @@ private void buildKeycloakJobContainer(Container keycloakContainer) {

public void updateStatus(KeycloakRealmImportStatusBuilder status) {
if (existingDeployment == null) {
status.addNotReadyMessage("No existing Deployment found, waiting for it to be created");
status.addErrorMessage("No existing Deployment found, waiting for it to be created");
return;
}

Expand All @@ -205,7 +186,7 @@ public void updateStatus(KeycloakRealmImportStatusBuilder status) {
} else if (oldStatus.getSucceeded() != null && oldStatus.getSucceeded() > 0) {
if (!lastReportedStatus.isDone()) {
Log.info("Job finished performing a rolling restart of the deployment");
rollingRestart();
rollingRestart(); // could be based upon a hash annotation on the deployment instead
}
status.addDone();
} else if (oldStatus.getFailed() != null && oldStatus.getFailed() > 0) {
Expand Down
Loading
0