Skip to content

Commit 56ebb88

Browse files
[TEP-0135] Purge finalizer and delete PVC
Part of [#6740][#6740], developed based on Priti's [prototype][prototype] and partially completes the PVC deletion behavior [discussion][discussion]. Prior to this commit, the `PVCs` created from `PipelineRun's` `VolumeClaimTemplate` are not auto deleted when the owning `PipelineRun` is completed. This commit updates the `cleanupAffinityAssistantsAndPVCs` function to remove the `kubernetes.io/pvc-protection` finalizer protection (so that the pvc is allowed to be deleted while the pod consuming it is not deleted). The function then explicitly delete such `PVC` when cleaning up the `Affinity Assistants` at pr completion time. This change is NOT applied to `coschedule: workspaces` mode as there is backward compatability concern. See more details in this [discussion][discussion] [#6740]: #6740 [prototype]: #6635 [discussion]: #6741 (comment)
1 parent 2250e40 commit 56ebb88

File tree

5 files changed

+198
-57
lines changed

5 files changed

+198
-57
lines changed

pkg/reconciler/pipelinerun/affinity_assistant.go

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -178,22 +178,43 @@ func (c *Reconciler) createOrUpdateAffinityAssistant(ctx context.Context, affini
178178
return errs
179179
}
180180

181-
// TODO(#6740)(WIP) implement cleanupAffinityAssistants for AffinityAssistantPerPipelineRun and AffinityAssistantPerPipelineRunWithIsolation affinity assistant modes
182-
func (c *Reconciler) cleanupAffinityAssistants(ctx context.Context, pr *v1.PipelineRun) error {
183-
// omit cleanup if the feature is disabled
184-
if c.isAffinityAssistantDisabled(ctx) {
185-
return nil
181+
// cleanupAffinityAssistantsAndPVCs deletes Affinity Assistant StatefulSets and PVCs created from VolumeClaimTemplates
182+
func (c *Reconciler) cleanupAffinityAssistantsAndPVCs(ctx context.Context, pr *v1.PipelineRun) error {
183+
aaBehavior, err := aa.GetAffinityAssistantBehavior(ctx)
184+
if err != nil {
185+
return err
186186
}
187187

188188
var errs []error
189-
for _, w := range pr.Spec.Workspaces {
190-
if w.PersistentVolumeClaim != nil || w.VolumeClaimTemplate != nil {
191-
affinityAssistantStsName := GetAffinityAssistantName(w.Name, pr.Name)
192-
if err := c.KubeClientSet.AppsV1().StatefulSets(pr.Namespace).Delete(ctx, affinityAssistantStsName, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
193-
errs = append(errs, fmt.Errorf("failed to delete StatefulSet %s: %w", affinityAssistantStsName, err))
189+
switch aaBehavior {
190+
case aa.AffinityAssistantPerWorkspace:
191+
for _, w := range pr.Spec.Workspaces {
192+
if w.PersistentVolumeClaim != nil || w.VolumeClaimTemplate != nil {
193+
affinityAssistantName := GetAffinityAssistantName(w.Name, pr.Name)
194+
if err := c.KubeClientSet.AppsV1().StatefulSets(pr.Namespace).Delete(ctx, affinityAssistantName, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
195+
errs = append(errs, fmt.Errorf("failed to delete StatefulSet %s: %w", affinityAssistantName, err))
196+
}
197+
}
198+
}
199+
case aa.AffinityAssistantPerPipelineRun, aa.AffinityAssistantPerPipelineRunWithIsolation:
200+
affinityAssistantName := GetAffinityAssistantName("", pr.Name)
201+
if err := c.KubeClientSet.AppsV1().StatefulSets(pr.Namespace).Delete(ctx, affinityAssistantName, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
202+
errs = append(errs, fmt.Errorf("failed to delete StatefulSet %s: %w", affinityAssistantName, err))
203+
}
204+
205+
// cleanup PVCs created by Affinity Assistants
206+
for _, w := range pr.Spec.Workspaces {
207+
if w.VolumeClaimTemplate != nil {
208+
pvcName := getPersistentVolumeClaimNameWithAffinityAssistant("", pr.Name, w, *kmeta.NewControllerRef(pr))
209+
if err := c.pvcHandler.PurgeFinalizerAndDeletePVCForWorkspace(ctx, pvcName, pr.Namespace); err != nil {
210+
errs = append(errs, err)
211+
}
194212
}
195213
}
214+
case aa.AffinityAssistantDisabled:
215+
return nil
196216
}
217+
197218
return errorutils.NewAggregate(errs)
198219
}
199220

pkg/reconciler/pipelinerun/affinity_assistant_test.go

Lines changed: 88 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/google/go-cmp/cmp"
2525
"github.com/google/go-cmp/cmp/cmpopts"
2626
"github.com/tektoncd/pipeline/pkg/apis/config"
27+
cfgtesting "github.com/tektoncd/pipeline/pkg/apis/config/testing"
2728
"github.com/tektoncd/pipeline/pkg/apis/pipeline"
2829
"github.com/tektoncd/pipeline/pkg/apis/pipeline/pod"
2930
v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
@@ -178,7 +179,7 @@ func TestCreateAndDeleteOfAffinityAssistantPerPipelineRun(t *testing.T) {
178179
expectAAName := GetAffinityAssistantName("", tc.pr.Name)
179180
validateStatefulSetSpec(t, ctx, c, expectAAName, tc.expectStatefulSetSpec)
180181

181-
// TODO(#6740)(WIP): test cleanupAffinityAssistants for coscheduling-pipelinerun mode when fully implemented
182+
// TODO(#6740)(WIP): test cleanupAffinityAssistantsAndPVCs for coscheduling-pipelinerun mode when fully implemented
182183
})
183184
}
184185
}
@@ -295,9 +296,9 @@ func TestCreateAndDeleteOfAffinityAssistantPerWorkspaceOrDisabled(t *testing.T)
295296
}
296297

297298
// clean up Affinity Assistant
298-
c.cleanupAffinityAssistants(ctx, tc.pr)
299+
c.cleanupAffinityAssistantsAndPVCs(ctx, tc.pr)
299300
if err != nil {
300-
t.Errorf("unexpected error from cleanupAffinityAssistants: %v", err)
301+
t.Errorf("unexpected error from cleanupAffinityAssistantsAndPVCs: %v", err)
301302
}
302303
for _, w := range tc.pr.Spec.Workspaces {
303304
if w.PersistentVolumeClaim == nil && w.VolumeClaimTemplate == nil {
@@ -630,7 +631,6 @@ func TestThatAffinityAssistantNameIsNoLongerThan53(t *testing.T) {
630631
t.Errorf("affinity assistant name can not be longer than 53 chars")
631632
}
632633
}
633-
634634
func TestCleanupAffinityAssistants_Success(t *testing.T) {
635635
workspace := v1.WorkspaceBinding{
636636
Name: "volumeClaimTemplate workspace",
@@ -646,53 +646,94 @@ func TestCleanupAffinityAssistants_Success(t *testing.T) {
646646
},
647647
}
648648

649-
// seed data to create StatefulSets and PVCs
650-
expectedAffinityAssistantName := GetAffinityAssistantName(workspace.Name, pr.Name)
651-
aa := []*appsv1.StatefulSet{{
652-
TypeMeta: metav1.TypeMeta{
653-
Kind: "StatefulSet",
654-
APIVersion: "apps/v1",
655-
},
656-
ObjectMeta: metav1.ObjectMeta{
657-
Name: expectedAffinityAssistantName,
658-
Labels: getStatefulSetLabels(pr, expectedAffinityAssistantName),
649+
testCases := []struct {
650+
name string
651+
aaBehavior aa.AffinityAssistantBehavior
652+
cfgMap map[string]string
653+
}{{
654+
name: "Affinity Assistant Cleanup - per workspace",
655+
aaBehavior: aa.AffinityAssistantPerWorkspace,
656+
cfgMap: map[string]string{
657+
"disable-affinity-assistant": "false",
658+
"coschedule": "workspaces",
659659
},
660-
Status: appsv1.StatefulSetStatus{
661-
ReadyReplicas: 1,
660+
}, {
661+
name: "Affinity Assistant Cleanup - per pipelinerun",
662+
aaBehavior: aa.AffinityAssistantPerPipelineRun,
663+
cfgMap: map[string]string{
664+
"disable-affinity-assistant": "true",
665+
"coschedule": "pipelineruns",
662666
},
663667
}}
664-
expectedPVCName := getPersistentVolumeClaimNameWithAffinityAssistant(workspace.Name, pr.Name, workspace, *kmeta.NewControllerRef(pr))
665-
pvc := []*corev1.PersistentVolumeClaim{{
666-
ObjectMeta: metav1.ObjectMeta{
667-
Name: expectedPVCName,
668-
}},
669-
}
670-
data := Data{
671-
StatefulSets: aa,
672-
PVCs: pvc,
673-
}
674-
ctx, c, _ := seedTestData(data)
675668

676-
// call clean up
677-
err := c.cleanupAffinityAssistants(ctx, pr)
678-
if err != nil {
679-
t.Fatalf("unexpected err when clean up Affinity Assistant: %v", err)
680-
}
669+
for _, tc := range testCases {
670+
expectedAffinityAssistantName := ""
671+
expectedPVCName := ""
672+
if tc.aaBehavior == aa.AffinityAssistantPerPipelineRun {
673+
expectedAffinityAssistantName = GetAffinityAssistantName("", pr.Name)
674+
expectedPVCName = getPersistentVolumeClaimNameWithAffinityAssistant("", pr.Name, workspace, *kmeta.NewControllerRef(pr))
675+
} else if tc.aaBehavior == aa.AffinityAssistantPerWorkspace {
676+
expectedAffinityAssistantName = GetAffinityAssistantName(workspace.Name, pr.Name)
677+
expectedPVCName = volumeclaim.GetPVCNameWithoutAffinityAssistant("", workspace, *kmeta.NewControllerRef(pr))
678+
}
681679

682-
// validate the cleanup result
683-
_, err = c.KubeClientSet.AppsV1().StatefulSets(pr.Namespace).Get(ctx, expectedAffinityAssistantName, metav1.GetOptions{})
684-
if !apierrors.IsNotFound(err) {
685-
t.Errorf("expected a NotFound response of StatefulSet, got: %v", err)
686-
}
680+
// seed data to create StatefulSets and PVCs
681+
ss := []*appsv1.StatefulSet{{
682+
TypeMeta: metav1.TypeMeta{
683+
Kind: "StatefulSet",
684+
APIVersion: "apps/v1",
685+
},
686+
ObjectMeta: metav1.ObjectMeta{
687+
Name: expectedAffinityAssistantName,
688+
Labels: getStatefulSetLabels(pr, expectedAffinityAssistantName),
689+
},
690+
Status: appsv1.StatefulSetStatus{
691+
ReadyReplicas: 1,
692+
},
693+
}}
694+
695+
pvc := []*corev1.PersistentVolumeClaim{{
696+
ObjectMeta: metav1.ObjectMeta{
697+
Name: expectedPVCName,
698+
}},
699+
}
700+
data := Data{
701+
StatefulSets: ss,
702+
PVCs: pvc,
703+
}
687704

688-
// the PVCs are NOT expected to be deleted at Affinity Assistant cleanup time
689-
_, err = c.KubeClientSet.CoreV1().PersistentVolumeClaims(pr.Namespace).Get(ctx, expectedPVCName, metav1.GetOptions{})
690-
if err != nil {
691-
t.Errorf("unexpected err when getting PersistentVolumeClaims, err: %v", err)
705+
_, c, _ := seedTestData(data)
706+
ctx := cfgtesting.SetFeatureFlags(context.Background(), t, tc.cfgMap)
707+
708+
// call clean up
709+
err := c.cleanupAffinityAssistantsAndPVCs(ctx, pr)
710+
if err != nil {
711+
t.Fatalf("unexpected err when clean up Affinity Assistant: %v", err)
712+
}
713+
714+
// validate the cleanup result
715+
_, err = c.KubeClientSet.AppsV1().StatefulSets(pr.Namespace).Get(ctx, expectedAffinityAssistantName, metav1.GetOptions{})
716+
if !apierrors.IsNotFound(err) {
717+
t.Errorf("expected a NotFound response of StatefulSet, got: %v", err)
718+
}
719+
720+
// validate pvcs
721+
if tc.aaBehavior == aa.AffinityAssistantPerWorkspace {
722+
// the PVCs are NOT expected to be deleted at Affinity Assistant cleanup time in AffinityAssistantPerWorkspace mode
723+
_, err = c.KubeClientSet.CoreV1().PersistentVolumeClaims(pr.Namespace).Get(ctx, expectedPVCName, metav1.GetOptions{})
724+
if err != nil {
725+
t.Errorf("unexpected err when getting PersistentVolumeClaims, err: %v", err)
726+
}
727+
} else {
728+
_, err = c.KubeClientSet.CoreV1().PersistentVolumeClaims(pr.Namespace).Get(ctx, expectedPVCName, metav1.GetOptions{})
729+
if !apierrors.IsNotFound(err) {
730+
t.Errorf("failed to clean up PersistentVolumeClaim")
731+
}
732+
}
692733
}
693734
}
694735

695-
func TestCleanupAffinityAssistants_Failure(t *testing.T) {
736+
func TestCleanupAffinityAssistantsAndPVCs_Failure(t *testing.T) {
696737
pr := &v1.PipelineRun{
697738
Spec: v1.PipelineRunSpec{
698739
Workspaces: []v1.WorkspaceBinding{{
@@ -716,7 +757,7 @@ func TestCleanupAffinityAssistants_Failure(t *testing.T) {
716757
errors.New("failed to delete StatefulSet affinity-assistant-e3b0c44298: error deleting statefulsets"),
717758
})
718759

719-
errs := c.cleanupAffinityAssistants(ctx, pr)
760+
errs := c.cleanupAffinityAssistantsAndPVCs(ctx, pr)
720761
if errs == nil {
721762
t.Fatalf("expecting Affinity Assistant cleanup error but got nil")
722763
}
@@ -747,7 +788,7 @@ func TestThatCleanupIsAvoidedIfAssistantIsDisabled(t *testing.T) {
747788
store := config.NewStore(logtesting.TestLogger(t))
748789
store.OnConfigChanged(configMap)
749790

750-
_ = c.cleanupAffinityAssistants(store.ToContext(context.Background()), testPRWithPVC)
791+
_ = c.cleanupAffinityAssistantsAndPVCs(store.ToContext(context.Background()), testPRWithPVC)
751792

752793
if len(fakeClientSet.Actions()) != 0 {
753794
t.Errorf("Expected 0 k8s client requests, did %d request", len(fakeClientSet.Actions()))
@@ -958,8 +999,10 @@ func seedTestData(d Data) (context.Context, Reconciler, func()) {
958999
ctx := context.Background()
9591000
ctx, cancel := context.WithCancel(ctx)
9601001

1002+
kubeClientSet := fakek8s.NewSimpleClientset()
9611003
c := Reconciler{
962-
KubeClientSet: fakek8s.NewSimpleClientset(),
1004+
KubeClientSet: kubeClientSet,
1005+
pvcHandler: volumeclaim.NewPVCHandler(kubeClientSet, zap.NewExample().Sugar()),
9631006
}
9641007
for _, s := range d.StatefulSets {
9651008
c.KubeClientSet.AppsV1().StatefulSets(s.Namespace).Create(ctx, s, metav1.CreateOptions{})

pkg/reconciler/pipelinerun/pipelinerun.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,9 +230,9 @@ func (c *Reconciler) ReconcileKind(ctx context.Context, pr *v1.PipelineRun) pkgr
230230

231231
if pr.IsDone() {
232232
pr.SetDefaults(ctx)
233-
err := c.cleanupAffinityAssistants(ctx, pr)
233+
err := c.cleanupAffinityAssistantsAndPVCs(ctx, pr)
234234
if err != nil {
235-
logger.Errorf("Failed to delete StatefulSet for PipelineRun %s: %v", pr.Name, err)
235+
logger.Errorf("Failed to delete StatefulSet or PVC for PipelineRun %s: %v", pr.Name, err)
236236
}
237237
return c.finishReconcileUpdateEmitEvents(ctx, pr, before, err)
238238
}

pkg/reconciler/volumeclaim/pvchandler.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,16 @@ package volumeclaim
1919
import (
2020
"context"
2121
"crypto/sha256"
22+
"encoding/json"
2223
"fmt"
2324

2425
v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
2526
"go.uber.org/zap"
27+
"gomodules.xyz/jsonpatch/v2"
2628
corev1 "k8s.io/api/core/v1"
2729
apierrors "k8s.io/apimachinery/pkg/api/errors"
2830
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31+
"k8s.io/apimachinery/pkg/types"
2932
errorutils "k8s.io/apimachinery/pkg/util/errors"
3033
clientset "k8s.io/client-go/kubernetes"
3134
)
@@ -39,6 +42,7 @@ const (
3942
// PvcHandler is used to create PVCs for workspaces
4043
type PvcHandler interface {
4144
CreatePVCsForWorkspaces(ctx context.Context, wb []v1.WorkspaceBinding, ownerReference metav1.OwnerReference, namespace string) error
45+
PurgeFinalizerAndDeletePVCForWorkspace(ctx context.Context, pvcName, namespace string) error
4246
}
4347

4448
type defaultPVCHandler struct {
@@ -81,6 +85,46 @@ func (c *defaultPVCHandler) CreatePVCsForWorkspaces(ctx context.Context, wb []v1
8185
return errorutils.NewAggregate(errs)
8286
}
8387

88+
func (c *defaultPVCHandler) PurgeFinalizerAndDeletePVCForWorkspace(ctx context.Context, pvcName, namespace string) error {
89+
p, err := c.clientset.CoreV1().PersistentVolumeClaims(namespace).Get(ctx, pvcName, metav1.GetOptions{})
90+
if err != nil {
91+
return fmt.Errorf("failed to get the PVC %s: %w", pvcName, err)
92+
}
93+
94+
// get the list of existing finalizers and drop `pvc-protection` if exists
95+
var finalizers []string
96+
for _, f := range p.ObjectMeta.Finalizers {
97+
if f == "kubernetes.io/pvc-protection" {
98+
continue
99+
}
100+
finalizers = append(finalizers, f)
101+
}
102+
103+
// prepare data to remove pvc-protection from the list of finalizers
104+
removeFinalizerBytes, err := json.Marshal([]jsonpatch.JsonPatchOperation{{
105+
Path: "/metadata/finalizers",
106+
Operation: "replace",
107+
Value: finalizers,
108+
}})
109+
if err != nil {
110+
return fmt.Errorf("failed to marshal jsonpatch: %w", err)
111+
}
112+
113+
// patch the existing PVC to update the finalizers
114+
_, err = c.clientset.CoreV1().PersistentVolumeClaims(namespace).Patch(ctx, pvcName, types.JSONPatchType, removeFinalizerBytes, metav1.PatchOptions{})
115+
if err != nil {
116+
return fmt.Errorf("failed to patch the PVC %s: %w", pvcName, err)
117+
}
118+
119+
// delete PVC
120+
err = c.clientset.CoreV1().PersistentVolumeClaims(namespace).Delete(ctx, pvcName, metav1.DeleteOptions{})
121+
if err != nil {
122+
return fmt.Errorf("failed to delete the PVC %s: %w", pvcName, err)
123+
}
124+
125+
return nil
126+
}
127+
84128
func getPVCsWithoutAffinityAssistant(workspaceBindings []v1.WorkspaceBinding, ownerReference metav1.OwnerReference, namespace string) map[string]*corev1.PersistentVolumeClaim {
85129
claims := make(map[string]*corev1.PersistentVolumeClaim)
86130
for _, workspaceBinding := range workspaceBindings {

pkg/reconciler/volumeclaim/pvchandler_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"go.uber.org/zap"
2626
corev1 "k8s.io/api/core/v1"
2727
"k8s.io/apimachinery/pkg/api/errors"
28+
apierrors "k8s.io/apimachinery/pkg/api/errors"
2829
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2930
"k8s.io/apimachinery/pkg/runtime"
3031
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -233,3 +234,35 @@ func TestCreateExistPersistentVolumeClaims(t *testing.T) {
233234
t.Fatalf("unexpected PVC name on created PVC; expected: %s got: %s", expectedPVCName, pvcList.Items[0].Name)
234235
}
235236
}
237+
238+
func TestPurgeFinalizerAndDeletePVCForWorkspace(t *testing.T) {
239+
ctx := context.Background()
240+
kubeClientSet := fakek8s.NewSimpleClientset()
241+
242+
// seed data
243+
namespace := "my-ns"
244+
pvcName := "my-pvc"
245+
pvc := &corev1.PersistentVolumeClaim{
246+
ObjectMeta: metav1.ObjectMeta{
247+
Name: pvcName,
248+
Namespace: namespace,
249+
Finalizers: []string{
250+
"kubernetes.io/pvc-protection",
251+
},
252+
}}
253+
if _, err := kubeClientSet.CoreV1().PersistentVolumeClaims(namespace).Create(ctx, pvc, metav1.CreateOptions{}); err != nil {
254+
t.Fatalf("unexpected error when creating PVC: %v", err)
255+
}
256+
257+
// call PurgeFinalizerAndDeletePVCForWorkspace
258+
pvcHandler := defaultPVCHandler{kubeClientSet, zap.NewExample().Sugar()}
259+
if err := pvcHandler.PurgeFinalizerAndDeletePVCForWorkspace(ctx, pvcName, namespace); err != nil {
260+
t.Fatalf("unexpected error when calling PurgeFinalizerAndDeletePVCForWorkspace: %v", err)
261+
}
262+
263+
// validate pvc is deleted
264+
_, err := kubeClientSet.CoreV1().PersistentVolumeClaims(namespace).Get(ctx, pvcName, metav1.GetOptions{})
265+
if !apierrors.IsNotFound(err) {
266+
t.Errorf("expected a NotFound response of PersistentVolumeClaims, got: %v", err)
267+
}
268+
}

0 commit comments

Comments
 (0)