Skip to content

Commit a21e10e

Browse files
authored
feat: adds pdbs to workbench sessions (#67)
1 parent cc3b8b1 commit a21e10e

File tree

8 files changed

+147
-13
lines changed

8 files changed

+147
-13
lines changed

api/product/disruption_budget.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package product
22

33
import (
4+
"fmt"
5+
46
policyv1 "k8s.io/api/policy/v1"
57
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
68
"k8s.io/apimachinery/pkg/util/intstr"
@@ -42,3 +44,29 @@ func DefineDisruptionBudget(p NamerAndOwnerProvider, req ctrl.Request, minAvaila
4244

4345
return pdb
4446
}
47+
48+
// DefineSessionDisruptionBudget creates a PodDisruptionBudget for workbench session pods.
49+
// This PDB uses maxUnavailable=0 to prevent any session pods from being evicted during
50+
// node drains or cluster maintenance, ensuring session persistence.
51+
func DefineSessionDisruptionBudget(p NamerAndOwnerProvider, req ctrl.Request) *policyv1.PodDisruptionBudget {
52+
return &policyv1.PodDisruptionBudget{
53+
ObjectMeta: metav1.ObjectMeta{
54+
Name: fmt.Sprintf("%s-sessions", p.ComponentName()),
55+
Namespace: req.Namespace,
56+
OwnerReferences: p.OwnerReferencesForChildren(),
57+
Labels: p.KubernetesLabels(),
58+
},
59+
Spec: policyv1.PodDisruptionBudgetSpec{
60+
Selector: &metav1.LabelSelector{
61+
MatchLabels: map[string]string{
62+
// This label is set by the launcher on all session pods
63+
"launcher-instance-id": p.ComponentName(),
64+
},
65+
},
66+
MaxUnavailable: &intstr.IntOrString{
67+
Type: intstr.Int,
68+
IntVal: 0,
69+
},
70+
},
71+
}
72+
}

client-go/applyconfiguration/core/v1beta1/connectruntimeimagespec.go

Lines changed: 62 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/controller/core/connect.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -822,9 +822,8 @@ func (r *ConnectReconciler) ensureDeployedService(ctx context.Context, req ctrl.
822822
}
823823

824824
// POD DISRUPTION BUDGET
825-
if err := CreateOrUpdateDisruptionBudget(
826-
ctx, req, r.Client, r.Scheme, c, c, ptr.To(product.DetermineMinAvailableReplicas(c.Spec.Replicas)), nil,
827-
); err != nil {
825+
pdb := product.DefineDisruptionBudget(c, req, ptr.To(product.DetermineMinAvailableReplicas(c.Spec.Replicas)), nil)
826+
if err := CreateOrUpdateDisruptionBudget(ctx, r.Client, r.Scheme, c, pdb); err != nil {
828827
return ctrl.Result{}, err
829828
}
830829

internal/controller/core/disruption_budget.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,12 @@ import (
99
policyv1 "k8s.io/api/policy/v1"
1010
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1111
"k8s.io/apimachinery/pkg/runtime"
12-
ctrl "sigs.k8s.io/controller-runtime"
1312
"sigs.k8s.io/controller-runtime/pkg/client"
1413
)
1514

16-
func CreateOrUpdateDisruptionBudget(ctx context.Context, req ctrl.Request, c client.Client, scheme *runtime.Scheme, p product.NamerAndOwnerProvider, owner client.Object, minAvailable, maxUnavailable *int) error {
15+
// CreateOrUpdateDisruptionBudget creates or updates a PodDisruptionBudget from the given spec.
16+
func CreateOrUpdateDisruptionBudget(ctx context.Context, c client.Client, scheme *runtime.Scheme, owner client.Object, pdbSpec *policyv1.PodDisruptionBudget) error {
1717
l := logr.FromContextOrDiscard(ctx)
18-
pdbSpec := product.DefineDisruptionBudget(p, req, minAvailable, maxUnavailable)
1918

2019
pdb := &policyv1.PodDisruptionBudget{
2120
ObjectMeta: metav1.ObjectMeta{

internal/controller/core/package_manager.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -686,9 +686,8 @@ func (r *PackageManagerReconciler) ensureDeployedService(ctx context.Context, re
686686
}
687687

688688
// POD DISRUPTION BUDGET
689-
if err := CreateOrUpdateDisruptionBudget(
690-
ctx, req, r.Client, r.Scheme, pm, pm, ptr.To(product.DetermineMinAvailableReplicas(pm.Spec.Replicas)), nil,
691-
); err != nil {
689+
pdb := product.DefineDisruptionBudget(pm, req, ptr.To(product.DetermineMinAvailableReplicas(pm.Spec.Replicas)), nil)
690+
if err := CreateOrUpdateDisruptionBudget(ctx, r.Client, r.Scheme, pm, pdb); err != nil {
692691
return ctrl.Result{}, err
693692
}
694693

internal/controller/core/site_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/traefik/traefik/v3/pkg/provider/kubernetes/crd/traefikio/v1alpha1"
1515
appsv1 "k8s.io/api/apps/v1"
1616
corev1 "k8s.io/api/core/v1"
17+
policyv1 "k8s.io/api/policy/v1"
1718
"k8s.io/apimachinery/pkg/api/resource"
1819
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1920
"k8s.io/apimachinery/pkg/runtime"
@@ -343,6 +344,13 @@ func getMiddleware(t *testing.T, cli client.Client, siteNamespace, siteName stri
343344
return middleware
344345
}
345346

347+
func getPodDisruptionBudget(t *testing.T, cli client.Client, namespace, name string) *policyv1.PodDisruptionBudget {
348+
pdb := &policyv1.PodDisruptionBudget{}
349+
err := cli.Get(context.TODO(), client.ObjectKey{Name: name, Namespace: namespace}, pdb, &client.GetOptions{})
350+
assert.Nil(t, err)
351+
return pdb
352+
}
353+
346354
func TestSiteReconcileWithTolerations(t *testing.T) {
347355
siteName := "tolerations-site"
348356
siteNamespace := "posit-team"

internal/controller/core/workbench.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -997,10 +997,15 @@ func (r *WorkbenchReconciler) ensureDeployedService(ctx context.Context, req ctr
997997
return ctrl.Result{}, err
998998
}
999999

1000-
// POD DISRUPTION BUDGET
1001-
if err := CreateOrUpdateDisruptionBudget(
1002-
ctx, req, r.Client, r.Scheme, w, w, ptr.To(product.DetermineMinAvailableReplicas(w.Spec.Replicas)), nil,
1003-
); err != nil {
1000+
// POD DISRUPTION BUDGET for server pods
1001+
serverPdb := product.DefineDisruptionBudget(w, req, ptr.To(product.DetermineMinAvailableReplicas(w.Spec.Replicas)), nil)
1002+
if err := CreateOrUpdateDisruptionBudget(ctx, r.Client, r.Scheme, w, serverPdb); err != nil {
1003+
return ctrl.Result{}, err
1004+
}
1005+
1006+
// POD DISRUPTION BUDGET for session pods (prevents eviction during node drains)
1007+
sessionPdb := product.DefineSessionDisruptionBudget(w, req)
1008+
if err := CreateOrUpdateDisruptionBudget(ctx, r.Client, r.Scheme, w, sessionPdb); err != nil {
10041009
return ctrl.Result{}, err
10051010
}
10061011

internal/controller/core/workbench_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,3 +355,37 @@ func TestWorkbenchLoadBalancingDisabled(t *testing.T) {
355355
assert.NotEqual(t, "load-balancer-config", v.Name, "Should not have load-balancer-config volume when load balancing is disabled")
356356
}
357357
}
358+
359+
func TestWorkbenchPodDisruptionBudgets(t *testing.T) {
360+
ctx := context.Background()
361+
ns := "posit-team"
362+
name := "workbench-pdb"
363+
364+
ctx, r, req, cli := initWorkbenchReconciler(t, ctx, ns, name)
365+
366+
wb := defineDefaultWorkbench(t, ns, name)
367+
368+
err := internal.BasicCreateOrUpdate(ctx, r, r.GetLogger(ctx), req.NamespacedName, &positcov1beta1.Workbench{}, wb)
369+
require.NoError(t, err)
370+
371+
wb = getWorkbench(t, cli, ns, name)
372+
373+
res, err := r.ReconcileWorkbench(ctx, req, wb)
374+
require.NoError(t, err)
375+
require.True(t, res.IsZero())
376+
377+
// Verify session PDB is created
378+
sessionPdb := getPodDisruptionBudget(t, cli, ns, name+"-workbench-sessions")
379+
require.NotNil(t, sessionPdb, "Session PDB should be created")
380+
assert.Equal(t, name+"-workbench-sessions", sessionPdb.Name)
381+
382+
// Verify session PDB has correct selector to target session pods
383+
require.NotNil(t, sessionPdb.Spec.Selector, "Session PDB should have a selector")
384+
assert.Equal(t, wb.ComponentName(), sessionPdb.Spec.Selector.MatchLabels["launcher-instance-id"],
385+
"Session PDB should select pods with launcher-instance-id label matching workbench component name")
386+
387+
// Verify session PDB has maxUnavailable=0 to prevent any evictions
388+
require.NotNil(t, sessionPdb.Spec.MaxUnavailable, "Session PDB should have maxUnavailable set")
389+
assert.Equal(t, int32(0), sessionPdb.Spec.MaxUnavailable.IntVal,
390+
"Session PDB should have maxUnavailable=0 to prevent session evictions")
391+
}

0 commit comments

Comments
 (0)