Skip to content

Commit

Permalink
feat(operator): Added support for creating managed pod disruption bud…
Browse files Browse the repository at this point in the history
…gets (#5911)

* Added support for creating default pod disruption budgets for all components

* spotless:apply

* Tried to improve the robustness of the PDB test

* spotless:apply

* Add the instance label to PDBs

* spotless:apply

* Resource types all-lowercase.  Delete PDB resource in activation condition

* fix(operator): typo

* fix(operator): missing RBAC for PDB

* Apply feedback from PR

* chore(operator): update install file

---------

Co-authored-by: Jakub Senko <[email protected]>
  • Loading branch information
EricWittmann and jsenko authored Feb 11, 2025
1 parent 990e34a commit 45abce9
Show file tree
Hide file tree
Showing 17 changed files with 535 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ rules:
- '*'

- apiGroups:
- ""
- ''
resources:
- pods
- services
Expand All @@ -48,4 +48,11 @@ rules:
resources:
- ingresses
verbs:
- "*"
- '*'

- apiGroups:
- policy
resources:
- poddisruptionbudgets
verbs:
- '*'
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,51 @@
import io.apicurio.registry.operator.resource.LabelDiscriminators.AppDeploymentDiscriminator;
import io.apicurio.registry.operator.resource.app.AppDeploymentResource;
import io.apicurio.registry.operator.resource.app.AppIngressResource;
import io.apicurio.registry.operator.resource.app.AppPodDisruptionBudgetResource;
import io.apicurio.registry.operator.resource.app.AppServiceResource;
import io.apicurio.registry.operator.resource.studioui.StudioUIDeploymentResource;
import io.apicurio.registry.operator.resource.studioui.StudioUIIngressResource;
import io.apicurio.registry.operator.resource.studioui.StudioUIPodDisruptionBudgetResource;
import io.apicurio.registry.operator.resource.studioui.StudioUIServiceResource;
import io.apicurio.registry.operator.resource.ui.UIDeploymentResource;
import io.apicurio.registry.operator.resource.ui.UIIngressResource;
import io.apicurio.registry.operator.resource.ui.UIPodDisruptionBudgetResource;
import io.apicurio.registry.operator.resource.ui.UIServiceResource;
import io.apicurio.registry.operator.updater.IngressCRUpdater;
import io.apicurio.registry.operator.updater.KafkaSqlCRUpdater;
import io.apicurio.registry.operator.updater.SqlCRUpdater;
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.javaoperatorsdk.operator.api.reconciler.*;
import io.javaoperatorsdk.operator.api.reconciler.Cleaner;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
import io.javaoperatorsdk.operator.api.reconciler.DeleteControl;
import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler;
import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusUpdateControl;
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static io.apicurio.registry.operator.resource.ActivationConditions.*;
import static io.apicurio.registry.operator.resource.ResourceKey.*;
import static io.apicurio.registry.operator.resource.ActivationConditions.AppIngressActivationCondition;
import static io.apicurio.registry.operator.resource.ActivationConditions.AppPodDisruptionBudgetActivationCondition;
import static io.apicurio.registry.operator.resource.ActivationConditions.StudioUIDeploymentActivationCondition;
import static io.apicurio.registry.operator.resource.ActivationConditions.StudioUIIngressActivationCondition;
import static io.apicurio.registry.operator.resource.ActivationConditions.StudioUIPodDisruptionBudgetActivationCondition;
import static io.apicurio.registry.operator.resource.ActivationConditions.UIIngressActivationCondition;
import static io.apicurio.registry.operator.resource.ActivationConditions.UIPodDisruptionBudgetActivationCondition;
import static io.apicurio.registry.operator.resource.ResourceKey.APP_DEPLOYMENT_ID;
import static io.apicurio.registry.operator.resource.ResourceKey.APP_INGRESS_ID;
import static io.apicurio.registry.operator.resource.ResourceKey.APP_POD_DISRUPTION_BUDGET_ID;
import static io.apicurio.registry.operator.resource.ResourceKey.APP_SERVICE_ID;
import static io.apicurio.registry.operator.resource.ResourceKey.STUDIO_UI_DEPLOYMENT_ID;
import static io.apicurio.registry.operator.resource.ResourceKey.STUDIO_UI_INGRESS_ID;
import static io.apicurio.registry.operator.resource.ResourceKey.STUDIO_UI_POD_DISRUPTION_BUDGET_ID;
import static io.apicurio.registry.operator.resource.ResourceKey.STUDIO_UI_SERVICE_ID;
import static io.apicurio.registry.operator.resource.ResourceKey.UI_DEPLOYMENT_ID;
import static io.apicurio.registry.operator.resource.ResourceKey.UI_INGRESS_ID;
import static io.apicurio.registry.operator.resource.ResourceKey.UI_POD_DISRUPTION_BUDGET_ID;
import static io.apicurio.registry.operator.resource.ResourceKey.UI_SERVICE_ID;

@ControllerConfiguration(
dependents = {
Expand All @@ -41,6 +68,12 @@
dependsOn = {APP_SERVICE_ID},
activationCondition = AppIngressActivationCondition.class
),
@Dependent(
type = AppPodDisruptionBudgetResource.class,
name = APP_POD_DISRUPTION_BUDGET_ID,
dependsOn = {APP_DEPLOYMENT_ID},
activationCondition = AppPodDisruptionBudgetActivationCondition.class
),
// ===== Registry UI
@Dependent(
type = UIDeploymentResource.class,
Expand All @@ -57,6 +90,12 @@
dependsOn = {UI_SERVICE_ID},
activationCondition = UIIngressActivationCondition.class
),
@Dependent(
type = UIPodDisruptionBudgetResource.class,
name = UI_POD_DISRUPTION_BUDGET_ID,
dependsOn = {UI_DEPLOYMENT_ID},
activationCondition = UIPodDisruptionBudgetActivationCondition.class
),
// ===== Studio UI
@Dependent(
type = StudioUIDeploymentResource.class,
Expand All @@ -73,7 +112,13 @@
name = STUDIO_UI_INGRESS_ID,
dependsOn = {STUDIO_UI_SERVICE_ID},
activationCondition = StudioUIIngressActivationCondition.class
)
),
@Dependent(
type = StudioUIPodDisruptionBudgetResource.class,
name = STUDIO_UI_POD_DISRUPTION_BUDGET_ID,
dependsOn = {STUDIO_UI_DEPLOYMENT_ID},
activationCondition = StudioUIPodDisruptionBudgetActivationCondition.class
),
}
)
// TODO: When renaming, do not forget to update application.properties (until we have a test for this).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,21 @@
import io.apicurio.registry.operator.api.v1.ApicurioRegistry3;
import io.apicurio.registry.operator.api.v1.ApicurioRegistry3Spec;
import io.apicurio.registry.operator.api.v1.spec.AppSpec;
import io.apicurio.registry.operator.api.v1.spec.ComponentSpec;
import io.apicurio.registry.operator.api.v1.spec.IngressSpec;
import io.apicurio.registry.operator.api.v1.spec.PodDisruptionSpec;
import io.apicurio.registry.operator.api.v1.spec.StudioUiSpec;
import io.apicurio.registry.operator.api.v1.spec.UiSpec;
import io.apicurio.registry.operator.resource.app.AppIngressResource;
import io.apicurio.registry.operator.resource.app.AppPodDisruptionBudgetResource;
import io.apicurio.registry.operator.resource.studioui.StudioUIDeploymentResource;
import io.apicurio.registry.operator.resource.studioui.StudioUIIngressResource;
import io.apicurio.registry.operator.resource.studioui.StudioUIPodDisruptionBudgetResource;
import io.apicurio.registry.operator.resource.ui.UIIngressResource;
import io.apicurio.registry.operator.resource.ui.UIPodDisruptionBudgetResource;
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.fabric8.kubernetes.api.model.networking.v1.Ingress;
import io.fabric8.kubernetes.api.model.policy.v1.PodDisruptionBudget;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
Expand Down Expand Up @@ -42,6 +48,25 @@ public boolean isMet(DependentResource<Ingress, ApicurioRegistry3> resource,
}
}

public static class AppPodDisruptionBudgetActivationCondition
implements Condition<PodDisruptionBudget, ApicurioRegistry3> {
@Override
public boolean isMet(DependentResource<PodDisruptionBudget, ApicurioRegistry3> resource,
ApicurioRegistry3 primary, Context<ApicurioRegistry3> context) {
boolean isEnabled = ofNullable(primary.getSpec()).map(ApicurioRegistry3Spec::getApp)
.map(ComponentSpec::getPodDisruptionBudget).map(PodDisruptionSpec::getEnabled)
.orElse(Boolean.TRUE);
int numReplicas = ofNullable(primary.getSpec()).map(ApicurioRegistry3Spec::getApp)
.map(ComponentSpec::getReplicas).orElse(1);

boolean isManaged = isEnabled && numReplicas > 1;
if (!isManaged) {
((AppPodDisruptionBudgetResource) resource).delete(primary, context);
}
return isManaged;
}
}

// ===== Registry UI

public static class UIIngressActivationCondition implements Condition<Ingress, ApicurioRegistry3> {
Expand All @@ -60,6 +85,21 @@ public boolean isMet(DependentResource<Ingress, ApicurioRegistry3> resource,
}
}

public static class UIPodDisruptionBudgetActivationCondition
implements Condition<PodDisruptionBudget, ApicurioRegistry3> {
@Override
public boolean isMet(DependentResource<PodDisruptionBudget, ApicurioRegistry3> resource,
ApicurioRegistry3 primary, Context<ApicurioRegistry3> context) {
boolean isManaged = ofNullable(primary.getSpec()).map(ApicurioRegistry3Spec::getUi)
.map(ComponentSpec::getPodDisruptionBudget).map(PodDisruptionSpec::getEnabled)
.orElse(Boolean.TRUE);
if (!isManaged) {
((UIPodDisruptionBudgetResource) resource).delete(primary, context);
}
return isManaged;
}
}

// ===== Studio UI

public static class StudioUIDeploymentActivationCondition
Expand Down Expand Up @@ -93,4 +133,20 @@ public boolean isMet(DependentResource<Ingress, ApicurioRegistry3> resource,
return enabled;
}
}

public static class StudioUIPodDisruptionBudgetActivationCondition
implements Condition<PodDisruptionBudget, ApicurioRegistry3> {
@Override
public boolean isMet(DependentResource<PodDisruptionBudget, ApicurioRegistry3> resource,
ApicurioRegistry3 primary, Context<ApicurioRegistry3> context) {
boolean isManaged = ofNullable(primary.getSpec()).map(ApicurioRegistry3Spec::getStudioUi)
.map(ComponentSpec::getPodDisruptionBudget).map(PodDisruptionSpec::getEnabled)
.orElse(Boolean.TRUE);
if (!isManaged) {
((StudioUIPodDisruptionBudgetResource) resource).delete(primary, context);
}
return isManaged;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import io.fabric8.kubernetes.api.model.Service;
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.fabric8.kubernetes.api.model.networking.v1.Ingress;
import io.fabric8.kubernetes.api.model.policy.v1.PodDisruptionBudget;
import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;

import java.util.Map;
Expand Down Expand Up @@ -53,6 +54,20 @@ public AppIngressDiscriminator() {
}
}

public static class AppPodDisruptionBudgetDiscriminator extends LabelDiscriminator<PodDisruptionBudget> {

public static final ResourceDiscriminator<PodDisruptionBudget, ApicurioRegistry3> INSTANCE = new AppPodDisruptionBudgetDiscriminator();

public AppPodDisruptionBudgetDiscriminator() {
// spotless:off
super(Map.of(
"app.kubernetes.io/name", "apicurio-registry",
"app.kubernetes.io/component", COMPONENT_APP
));
// spotless:on
}
}

// ===== Registry UI

public static class UIDeploymentDiscriminator extends LabelDiscriminator<Deployment> {
Expand Down Expand Up @@ -91,6 +106,20 @@ public UIIngressDiscriminator() {
}
}

public static class UiPodDisruptionBudgetDiscriminator extends LabelDiscriminator<PodDisruptionBudget> {

public static final ResourceDiscriminator<PodDisruptionBudget, ApicurioRegistry3> INSTANCE = new AppPodDisruptionBudgetDiscriminator();

public UiPodDisruptionBudgetDiscriminator() {
// spotless:off
super(Map.of(
"app.kubernetes.io/name", "apicurio-registry",
"app.kubernetes.io/component", COMPONENT_UI
));
// spotless:on
}
}

// ===== Studio UI

public static class StudioUIDeploymentDiscriminator extends LabelDiscriminator<Deployment> {
Expand Down Expand Up @@ -128,4 +157,20 @@ public StudioUIIngressDiscriminator() {
));
}
}

public static class StudioUiPodDisruptionBudgetDiscriminator
extends LabelDiscriminator<PodDisruptionBudget> {

public static final ResourceDiscriminator<PodDisruptionBudget, ApicurioRegistry3> INSTANCE = new AppPodDisruptionBudgetDiscriminator();

public StudioUiPodDisruptionBudgetDiscriminator() {
// spotless:off
super(Map.of(
"app.kubernetes.io/name", "apicurio-registry",
"app.kubernetes.io/component", COMPONENT_STUDIO_UI
));
// spotless:on
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.fabric8.kubernetes.api.model.apps.DeploymentSpec;
import io.fabric8.kubernetes.api.model.networking.v1.Ingress;
import io.fabric8.kubernetes.api.model.policy.v1.PodDisruptionBudget;

import java.nio.charset.Charset;
import java.util.ArrayList;
Expand All @@ -37,6 +38,7 @@ public class ResourceFactory {
public static final String RESOURCE_TYPE_DEPLOYMENT = "deployment";
public static final String RESOURCE_TYPE_SERVICE = "service";
public static final String RESOURCE_TYPE_INGRESS = "ingress";
public static final String RESOURCE_TYPE_POD_DISRUPTION_BUDGET = "poddisruptionbudget";

public Deployment getDefaultAppDeployment(ApicurioRegistry3 primary) {
var r = initDefaultDeployment(primary, COMPONENT_APP,
Expand Down Expand Up @@ -223,6 +225,30 @@ public Ingress getDefaultStudioUIIngress(ApicurioRegistry3 primary) {
return r;
}

public PodDisruptionBudget getDefaultAppPodDisruptionBudget(ApicurioRegistry3 primary) {
var pdb = getDefaultResource(primary, PodDisruptionBudget.class, RESOURCE_TYPE_POD_DISRUPTION_BUDGET,
COMPONENT_APP);
pdb.getSpec().getSelector().getMatchLabels().put("app.kubernetes.io/instance",
primary.getMetadata().getName());
return pdb;
}

public PodDisruptionBudget getDefaultUIPodDisruptionBudget(ApicurioRegistry3 primary) {
var pdb = getDefaultResource(primary, PodDisruptionBudget.class, RESOURCE_TYPE_POD_DISRUPTION_BUDGET,
COMPONENT_UI);
pdb.getSpec().getSelector().getMatchLabels().put("app.kubernetes.io/instance",
primary.getMetadata().getName());
return pdb;
}

public PodDisruptionBudget getDefaultStudioUIPodDisruptionBudget(ApicurioRegistry3 primary) {
var pdb = getDefaultResource(primary, PodDisruptionBudget.class, RESOURCE_TYPE_POD_DISRUPTION_BUDGET,
COMPONENT_STUDIO_UI);
pdb.getSpec().getSelector().getMatchLabels().put("app.kubernetes.io/instance",
primary.getMetadata().getName());
return pdb;
}

private <T extends HasMetadata> T getDefaultResource(ApicurioRegistry3 primary, Class<T> klass,
String resourceType, String component) {
var r = deserialize("/k8s/default/" + component + "." + resourceType + ".yaml", klass);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import io.fabric8.kubernetes.api.model.Service;
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.fabric8.kubernetes.api.model.networking.v1.Ingress;
import io.fabric8.kubernetes.api.model.policy.v1.PodDisruptionBudget;
import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
Expand All @@ -27,14 +28,17 @@ public class ResourceKey<R> {
public static final String APP_DEPLOYMENT_ID = "AppDeploymentResource";
public static final String APP_SERVICE_ID = "AppServiceResource";
public static final String APP_INGRESS_ID = "AppIngressResource";
public static final String APP_POD_DISRUPTION_BUDGET_ID = "AppPodDisruptionBudgetResource";

public static final String UI_DEPLOYMENT_ID = "UIDeploymentResource";
public static final String UI_SERVICE_ID = "UIServiceResource";
public static final String UI_INGRESS_ID = "UIIngressResource";
public static final String UI_POD_DISRUPTION_BUDGET_ID = "UIPodDisruptionBudgetResource";

public static final String STUDIO_UI_DEPLOYMENT_ID = "StudioUIDeploymentResource";
public static final String STUDIO_UI_SERVICE_ID = "StudioUIServiceResource";
public static final String STUDIO_UI_INGRESS_ID = "StudioUIIngressResource";
public static final String STUDIO_UI_POD_DISRUPTION_BUDGET_ID = "StudioUIPodDisruptionBudgetResource";

public static final ResourceKey<ApicurioRegistry3> REGISTRY_KEY = new ResourceKey<>(
REGISTRY_ID, ApicurioRegistry3.class,
Expand All @@ -58,6 +62,11 @@ public class ResourceKey<R> {
AppIngressDiscriminator.INSTANCE, ResourceFactory.INSTANCE::getDefaultAppIngress
);

public static final ResourceKey<PodDisruptionBudget> APP_POD_DISRUPTION_BUDGET_KEY = new ResourceKey<>(
APP_POD_DISRUPTION_BUDGET_ID, PodDisruptionBudget.class,
AppPodDisruptionBudgetDiscriminator.INSTANCE, ResourceFactory.INSTANCE::getDefaultAppPodDisruptionBudget
);

// ===== Registry UI

public static final ResourceKey<Deployment> UI_DEPLOYMENT_KEY = new ResourceKey<>(
Expand All @@ -75,6 +84,11 @@ public class ResourceKey<R> {
UIIngressDiscriminator.INSTANCE, ResourceFactory.INSTANCE::getDefaultUIIngress
);

public static final ResourceKey<PodDisruptionBudget> UI_POD_DISRUPTION_BUDGET_KEY = new ResourceKey<>(
UI_POD_DISRUPTION_BUDGET_ID, PodDisruptionBudget.class,
UiPodDisruptionBudgetDiscriminator.INSTANCE, ResourceFactory.INSTANCE::getDefaultUIPodDisruptionBudget
);

// ===== Studio UI

public static final ResourceKey<Deployment> STUDIO_UI_DEPLOYMENT_KEY = new ResourceKey<>(
Expand All @@ -92,6 +106,10 @@ public class ResourceKey<R> {
StudioUIIngressDiscriminator.INSTANCE, ResourceFactory.INSTANCE::getDefaultStudioUIIngress
);

public static final ResourceKey<PodDisruptionBudget> STUDIO_UI_POD_DISRUPTION_BUDGET_KEY = new ResourceKey<>(
STUDIO_UI_POD_DISRUPTION_BUDGET_ID, PodDisruptionBudget.class,
StudioUiPodDisruptionBudgetDiscriminator.INSTANCE, ResourceFactory.INSTANCE::getDefaultStudioUIPodDisruptionBudget
);

@EqualsAndHashCode.Include
@ToString.Include
Expand Down
Loading

0 comments on commit 45abce9

Please sign in to comment.