8000 fix: ensures that owner references are updated after an upgrade by shawkins · Pull Request #48213 · 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
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public final class Constants {
public static final String MANAGED_BY_LABEL = "app.kubernetes.io/managed-by";
public static final String MANAGED_BY_VALUE = "keycloak-operator";
public static final String COMPONENT_LABEL = "app.kubernetes.io/component";
public static final String KEYCLOAK_PAUSE_ANNOTATION = "operator.keycloak.org/pause";
public static final String KEYCLOAK_MIGRATING_ANNOTATION = "operator.keycloak.org/migrating";
public static final String KEYCLOAK_RECREATE_UPDATE_ANNOTATION = "operator.keycloak.org/recreate-update";
public static final String KEYCLOAK_UPDATE_REASON_ANNOTATION = "operator.keycloak.org/update-reason";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@
import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil;
import io.javaoperatorsdk.operator.api.config.informer.Informer;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
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 io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper;

@KubernetesDependent(
informer = @Informer(labelSelector = Constants.DEFAULT_LABELS_AS_STRING)
Expand Down Expand Up @@ -59,5 +61,11 @@ public static boolean hasCustomAdminSecret(Keycloak keycloak) {
return Optional.ofNullable(keycloak.getSpec().getBootstrapAdminSpec()).map(BootstrapAdminSpec::getUser)
.map(BootstrapAdminSpec.User::getSecret).filter(s -> !s.equals(KeycloakAdminSecretDependentResource.getName(keycloak))).isPresent();
}

@Override
protected Optional<SecondaryToPrimaryMapper<Secret>> getSecondaryToPrimaryMapper(
EventSourceContext<Keycloak> context) {
return VersionTolerantCRUDKubernetesDependentResource.primaryMapper(context);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil;
import io.fabric8.kubernetes.client.utils.Serialization;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusUpdateControl;
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
import io.javaoperatorsdk.operator.api.reconciler.EventSourceUtils;
Expand All @@ -73,6 +74,8 @@
activationCondition = KeycloakServiceMonitorDependentResource.ActivationCondition.class
),
})
// to allow for reactions to annotation changes
@ControllerConfiguration(generationAwareEventProcessing = false)
public class KeycloakController implements Reconciler<Keycloak> {

public static final String OPENSHIFT_DEFAULT = "openshift-default";
Expand Down Expand Up @@ -101,6 +104,9 @@ public List<EventSource<?, Keycloak>> prepareEventSources(EventSourceContext<Key

@Override
public UpdateControl<Keycloak> reconcile(Keycloak kc, Context<Keycloak> context) {
if (Boolean.valueOf(kc.getMetadata().getAnnotations().get(Constants.KEYCLOAK_PAUSE_ANNOTATION))) {
return UpdateControl.noUpdate(); // do nothing while paused
}
String kcName = kc.getMetadata().getName();
String namespace = kc.getMetadata().getNamespace();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
package org.keycloak.operator.controllers;

import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
Expand Down Expand Up @@ -70,7 +68,6 @@
import io.javaoperatorsdk.operator.api.config.informer.Informer;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
import io.quarkus.logging.Log;
Expand All @@ -84,7 +81,7 @@
@KubernetesDependent(
informer = @Informer(labelSelector = Constants.DEFAULT_LABELS_AS_STRING)
)
public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependentResource<StatefulSet, Keycloak> {
public class KeycloakDeploymentDependentResource extends VersionTolerantCRUDKubernetesDependentResource<StatefulSet, Keycloak> {

public static final String HTTP_MANAGEMENT_SCHEME = "http-management-scheme";

Expand All @@ -93,9 +90,6 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent

private static final List<String> COPY_ENV = Arrays.asList("HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY");

private static final String SERVICE_ACCOUNT_DIR = "/var/run/secrets/kubernetes.io/serviceaccount/";
private static final String SERVICE_CA_CRT = SERVICE_ACCOUNT_DIR + "service-ca.crt";

public static final String CACHE_CONFIG_FILE_MOUNT_NAME = "cache-config-file-configmap";

public static final String KC_TRUSTSTORE_PATHS = "KC_TRUSTSTORE_PATHS";
Expand All @@ -108,8 +102,6 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent

public static final String OPTIMIZED_ARG = "--optimized";

private boolean useServiceCaCrt;

// Do not create the deployment before the initial admin secret is created to prevent the deployment from restarting.
// Not using native dependsOn as the initial admin secret may not be created by the operator and might be provided by the user,
// in which case we want to create the deployment immediately.
Expand All @@ -124,11 +116,6 @@ public boolean isMet(DependentResource<StatefulSet, Keycloak> dependentResource,

public KeycloakDeploymentDependentResource() {
super(StatefulSet.class);
useServiceCaCrt = Files.exists(Path.of(SERVICE_CA_CRT));
}

public void setUseServiceCaCrt(boolean useServiceCaCrt) {
this.useServiceCaCrt = useServiceCaCrt;
}

public StatefulSet initialDesired(Keycloak primary, Context<Keycloak> context) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,12 @@
import io.fabric8.kubernetes.api.model.ServiceSpecBuilder;
import io.javaoperatorsdk.operator.api.config.informer.Informer;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;

@KubernetesDependent(
informer = @Informer(labelSelector = Constants.DEFAULT_LABELS_AS_STRING)
)
public class KeycloakDiscoveryServiceDependentResource extends CRUDKubernetesDependentResource<Service, Keycloak> {
public class KeycloakDiscoveryServiceDependentResource extends VersionTolerantCRUDKubernetesDependentResource<Service, Keycloak> {

public KeycloakDiscoveryServiceDependentResource() {
super(Service.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
import io.javaoperatorsdk.operator.api.config.informer.Informer;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
import io.quarkus.logging.Log;
Expand All @@ -44,7 +43,7 @@
@KubernetesDependent(
informer = @Informer(labelSelector = Constants.DEFAULT_LABELS_AS_STRING)
)
public class KeycloakIngressDependentResource extends CRUDKubernetesDependentResource<Ingress, Keycloak> {
public class KeycloakIngressDependentResource extends VersionTolerantCRUDKubernetesDependentResource<Ingress, Keycloak> {

private static final Logger LOG = Logger.getLogger(KeycloakIngressDependentResource.class.getName());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
import io.javaoperatorsdk.operator.api.config.informer.Informer;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
import org.jboss.logging.Logger;
Expand All @@ -51,7 +50,7 @@
@KubernetesDependent(
informer = @Informer(labelSelector = Constants.DEFAULT_LABELS_AS_STRING)
)
public class KeycloakNetworkPolicyDependentResource extends CRUDKubernetesDependentResource<NetworkPolicy, Keycloak> {
public class KeycloakNetworkPolicyDependentResource extends VersionTolerantCRUDKubernetesDependentResource<NetworkPolicy, Keycloak> {

private static final Logger LOG = Logger.getLogger(KeycloakNetworkPolicyDependentResource.class.getName());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,12 @@
import io.fabric8.kubernetes.api.model.batch.v1.JobBuilder;
import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
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.event.source.SecondaryToPrimaryMapper;

import static org.keycloak.operator.Utils.addResources;
import static org.keycloak.operator.controllers.KeycloakDistConfigurator.getKeycloakOptionEnvVarName;
Expand Down Expand Up @@ -182,4 +184,10 @@ static void handleJobScheduling(Keycloak keycloak, Optional<SchedulingSpec> sche
}
// else use the parent values
}

@Override
protected Optional<SecondaryToPrimaryMapper<Job>> getSecondaryToPrimaryMapper(
EventSourceContext<KeycloakRealmImport> context) {
return VersionTolerantCRUDKubernetesDependentResource.primaryMapper(context);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@
import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil;
import io.javaoperatorsdk.operator.api.config.informer.Informer;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;

@KubernetesDependent(
informer = @Informer(labelSelector = Constants.DEFAULT_LABELS_AS_STRING)
)
public class KeycloakRealmImportSecretDependentResource extends CRUDKubernetesDependentResource<Secret, KeycloakRealmImport> {
public class KeycloakRealmImportSecretDependentResource extends VersionTolerantCRUDKubernetesDependentResource<Secret, KeycloakRealmImport> {

public static final String DEPENDENT_NAME = "realm-import-secret";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,14 @@
import io.fabric8.kubernetes.api.model.ServiceSpecBuilder;
import io.javaoperatorsdk.operator.api.config.informer.Informer;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;

import static org.keycloak.operator.crds.v2beta1.CRDUtils.isTlsConfigured;

@KubernetesDependent(
informer = @Informer(labelSelector = Constants.DEFAULT_LABELS_AS_STRING)
)
public class KeycloakServiceDependentResource extends CRUDKubernetesDependentResource<Service, Keycloak> {
public class KeycloakServiceDependentResource extends VersionTolerantCRUDKubernetesDependentResource<Service, Keycloak> {

public KeycloakServiceDependentResource() {
super(Service.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import io.javaoperatorsdk.operator.api.config.informer.Informer;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
import io.quarkiverse.operatorsdk.annotations.CSVMetadata;
Expand All @@ -32,7 +31,7 @@
informer = @Informer(labelSelector = Constants.DEFAULT_LABELS_AS_STRING)
)
@CSVMetadata.Optional
public class KeycloakServiceMonitorDependentResource extends CRUDKubernetesDependentResource<ServiceMonitor, Keycloak> {
public class KeycloakServiceMonitorDependentResource extends VersionTolerantCRUDKubernetesDependentResource<ServiceMonitor, Keycloak> {

public static final String OPEN_METRICS_PROTOCOL = "OpenMetricsText1.0.0";
public static final String WARN_METRICS_NOT_ENABLED = "A ServiceMonitor will not be created because `metrics-enabled` is not true.";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,10 @@
import io.fabric8.kubernetes.api.model.batch.v1.JobSpecFluent;
import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfigBuilder;

@ApplicationScoped
public class KeycloakUpdateJobDependentResource extends CRUDKubernetesDependentResource<Job, Keycloak> {
public class KeycloakUpdateJobDependentResource extends VersionTolerantCRUDKubernetesDependentResource<Job, Keycloak> {

// shared volume configuration
private static final String WORK_DIR_VOLUME_NAME = "keycloak-update-job-temporary-workdir"; // unlikely to conflict
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.keycloak.operator.controllers;

import java.util.Optional;
import java.util.stream.Collectors;

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.Namespaced;
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.event.ResourceID;
import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper;

public class VersionTolerantCRUDKubernetesDependentResource<R extends HasMetadata, P extends HasMetadata>
extends CRUDKubernetesDependentResource<R, P> {

public VersionTolerantCRUDKubernetesDependentResource() {
super();
}

public VersionTolerantCRUDKubernetesDependentResource(Class<R> clazz) {
super(clazz);
}

/**
* Overrides the default handling to use startsWith api, rather than equality on the apiVersion
*/
@Override
protected Optional<SecondaryToPrimaryMapper<R>> getSecondaryToPrimaryMapper(EventSourceContext<P> context) {
return primaryMapper(context);
}

static <R extends HasMetadata, P extends HasMetadata> Optional<SecondaryToPrimaryMapper<R>> primaryMapper(EventSourceContext<P> context) {
Class<?> primaryClass = context.getPrimaryResourceClass();
String apiVersion = HasMetadata.getApiVersion(primaryClass);
String kind = HasMetadata.getKind(primaryClass);
apiVersion = apiVersion.startsWith("/") ? apiVersion.substring(1) : apiVersion;
String correctApi = apiVersion.substring(0, apiVersion.indexOf("/") + 1);
boolean clusterScoped = !Namespaced.class.isAssignableFrom(primaryClass);
return Optional.of(resource -> resource.getMetadata().getOwnerReferences().stream()
.filter(owner -> owner.getKind().equals(kind) && owner.getApiVersion().startsWith(correctApi))
.map(or -> ResourceID.fromOwnerReference(resource, or, clusterScoped)).collect(Collectors.toSet()));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.keycloak.operator.testsuite.integration;

import java.time.Duration;

import org.keycloak.operator.Constants;
import org.keycloak.operator.testsuite.utils.K8sUtils;

import io.fabric8.kubernetes.api.model.Service;
import io.fabric8.kubernetes.client.utils.Serialization;
import io.quarkus.test.junit.QuarkusTest;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertTrue;

@QuarkusTest
public class CRDUpgradeTest extends BaseOperatorTest {

@Test
public void testKeycloakUpgrade() {
var kc = getTestKeycloakDeployment(true);
kc.getMetadata().getAnnotations().put(Constants.KEYCLOAK_PAUSE_ANNOTATION, "true");
var deployedKc = k8sclient.resource(kc).create();

Service service = Serialization.unmarshal(this.getClass().getResourceAsStream("/v2alpha1-service.yaml"), Service.class);
service.getMetadata().setNamespace(deployedKc.getMetadata().getNamespace());
service.addOwnerReference(deployedKc).setApiVersion("k8s.keycloak.org/v2alpha1");
var serviceSelector = k8sclient.services().resource(service);
serviceSelector.create();

// now that the v2alpha1 service exists, try to reconcile - this should not fail and the owner
// reference should be updated
kc.getMetadata().getAnnotations().remove(Constants.KEYCLOAK_PAUSE_ANNOTATION);
K8sUtils.deployKeycloak(k8sclient, kc, true);

Awaitility.await().timeout(Duration.ofMinutes(1))
.untilAsserted(() -> assertTrue(serviceSelector.get().getMetadata().getOwnerReferences().stream().allMatch(or -> or.getApiVersion().equals("k8s.keycloak.org/v2beta1"))));
}
}
28 changes: 28 additions & 0 deletions operator/src/test/resources/v2alpha1-service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
apiVersion: "v1"
kind: "Service"
metadata:
labels:
app: "keycloak"
app.kubernetes.io/instance: "example-kc"
app.kubernetes.io/managed-by: "keycloak-operator"
name: "example-kc-service"
spec:
internalTrafficPolicy: "Cluster"
ipFamilies:
- "IPv4"
ipFamilyPolicy: "SingleStack"
ports:
- name: "https"
port: 8443
protocol: "TCP"
targetPort: 8443
- name: "management"
port: 9000
protocol: "TCP"
targetPort: 9000
selector:
app: "keycloak"
app.kubernetes.io/instance: "example-kc"
app.kubernetes.io/managed-by: "keycloak-operator"
sessionAffinity: "None"
type: "ClusterIP"
Loading
0