Skip to content

Commit 100d575

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 100d575

File tree

5 files changed

+221
-62
lines changed

5 files changed

+221
-62
lines changed

pkg/reconciler/pipelinerun/affinity_assistant.go

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -178,22 +178,44 @@ 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+
// TODO (#5776): support optional PVC deletion behavior for per-workspace mode
192+
for _, w := range pr.Spec.Workspaces {
193+
if w.PersistentVolumeClaim != nil || w.VolumeClaimTemplate != nil {
194+
affinityAssistantName := GetAffinityAssistantName(w.Name, pr.Name)
195+
if err := c.KubeClientSet.AppsV1().StatefulSets(pr.Namespace).Delete(ctx, affinityAssistantName, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
196+
errs = append(errs, fmt.Errorf("failed to delete StatefulSet %s: %w", affinityAssistantName, err))
197+
}
198+
}
199+
}
200+
case aa.AffinityAssistantPerPipelineRun, aa.AffinityAssistantPerPipelineRunWithIsolation:
201+
affinityAssistantName := GetAffinityAssistantName("", pr.Name)
202+
if err := c.KubeClientSet.AppsV1().StatefulSets(pr.Namespace).Delete(ctx, affinityAssistantName, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
203+
errs = append(errs, fmt.Errorf("failed to delete StatefulSet %s: %w", affinityAssistantName, err))
204+
}
205+
206+
// cleanup PVCs created by Affinity Assistants
207+
for _, w := range pr.Spec.Workspaces {
208+
if w.VolumeClaimTemplate != nil {
209+
pvcName := getPersistentVolumeClaimNameWithAffinityAssistant("", pr.Name, w, *kmeta.NewControllerRef(pr))
210+
if err := c.pvcHandler.PurgeFinalizerAndDeletePVCForWorkspace(ctx, pvcName, pr.Namespace); err != nil {
211+
errs = append(errs, err)
212+
}
194213
}
195214
}
215+
case aa.AffinityAssistantDisabled:
216+
return nil
196217
}
218+
197219
return errorutils.NewAggregate(errs)
198220
}
199221

pkg/reconciler/pipelinerun/affinity_assistant_test.go

Lines changed: 107 additions & 50 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"
@@ -42,7 +43,6 @@ import (
4243
fakek8s "k8s.io/client-go/kubernetes/fake"
4344
"k8s.io/client-go/kubernetes/typed/core/v1/fake"
4445
testing2 "k8s.io/client-go/testing"
45-
"knative.dev/pkg/kmeta"
4646
logtesting "knative.dev/pkg/logging/testing"
4747
"knative.dev/pkg/system"
4848
_ "knative.dev/pkg/system/testing" // Setup system.Namespace()
@@ -178,7 +178,7 @@ func TestCreateAndDeleteOfAffinityAssistantPerPipelineRun(t *testing.T) {
178178
expectAAName := GetAffinityAssistantName("", tc.pr.Name)
179179
validateStatefulSetSpec(t, ctx, c, expectAAName, tc.expectStatefulSetSpec)
180180

181-
// TODO(#6740)(WIP): test cleanupAffinityAssistants for coscheduling-pipelinerun mode when fully implemented
181+
// TODO(#6740)(WIP): test cleanupAffinityAssistantsAndPVCs for coscheduling-pipelinerun mode when fully implemented
182182
})
183183
}
184184
}
@@ -295,9 +295,9 @@ func TestCreateAndDeleteOfAffinityAssistantPerWorkspaceOrDisabled(t *testing.T)
295295
}
296296

297297
// clean up Affinity Assistant
298-
c.cleanupAffinityAssistants(ctx, tc.pr)
298+
c.cleanupAffinityAssistantsAndPVCs(ctx, tc.pr)
299299
if err != nil {
300-
t.Errorf("unexpected error from cleanupAffinityAssistants: %v", err)
300+
t.Errorf("unexpected error from cleanupAffinityAssistantsAndPVCs: %v", err)
301301
}
302302
for _, w := range tc.pr.Spec.Workspaces {
303303
if w.PersistentVolumeClaim == nil && w.VolumeClaimTemplate == nil {
@@ -630,69 +630,124 @@ func TestThatAffinityAssistantNameIsNoLongerThan53(t *testing.T) {
630630
t.Errorf("affinity assistant name can not be longer than 53 chars")
631631
}
632632
}
633-
634633
func TestCleanupAffinityAssistants_Success(t *testing.T) {
635-
workspace := v1.WorkspaceBinding{
636-
Name: "volumeClaimTemplate workspace",
637-
VolumeClaimTemplate: &corev1.PersistentVolumeClaim{},
634+
workspaces := []v1.WorkspaceBinding{
635+
{
636+
Name: "volumeClaimTemplate workspace 1",
637+
VolumeClaimTemplate: &corev1.PersistentVolumeClaim{},
638+
},
639+
{
640+
Name: "volumeClaimTemplate workspace 2",
641+
VolumeClaimTemplate: &corev1.PersistentVolumeClaim{},
642+
},
638643
}
639644
pr := &v1.PipelineRun{
640645
TypeMeta: metav1.TypeMeta{Kind: "PipelineRun"},
641646
ObjectMeta: metav1.ObjectMeta{
642647
Name: "test-pipelinerun-volumeclaimtemplate",
643648
},
644649
Spec: v1.PipelineRunSpec{
645-
Workspaces: []v1.WorkspaceBinding{workspace},
650+
Workspaces: workspaces,
646651
},
647652
}
648653

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),
654+
testCases := []struct {
655+
name string
656+
aaBehavior aa.AffinityAssistantBehavior
657+
cfgMap map[string]string
658+
expectedAffinityAssistantNames []string
659+
expectedPVCNames []string
660+
}{{
661+
name: "Affinity Assistant Cleanup - per workspace",
662+
aaBehavior: aa.AffinityAssistantPerWorkspace,
663+
cfgMap: map[string]string{
664+
"disable-affinity-assistant": "false",
665+
"coschedule": "workspaces",
659666
},
660-
Status: appsv1.StatefulSetStatus{
661-
ReadyReplicas: 1,
667+
expectedAffinityAssistantNames: []string{"affinity-assistant-9d8b15fa2e", "affinity-assistant-39883fc3b2"},
668+
expectedPVCNames: []string{"pvc-a12c589442", "pvc-5ce7cd98c5"},
669+
}, {
670+
name: "Affinity Assistant Cleanup - per pipelinerun",
671+
aaBehavior: aa.AffinityAssistantPerPipelineRun,
672+
cfgMap: map[string]string{
673+
"disable-affinity-assistant": "true",
674+
"coschedule": "pipelineruns",
662675
},
676+
expectedAffinityAssistantNames: []string{"affinity-assistant-62843d388a"},
677+
expectedPVCNames: []string{"pvc-a12c589442-affinity-assistant-62843d388a-0", "pvc-5ce7cd98c5-affinity-assistant-62843d388a-0"},
663678
}}
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)
675679

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-
}
680+
for _, tc := range testCases {
681+
// seed data to create StatefulSets and PVCs
682+
var statefulsets []*appsv1.StatefulSet
683+
for _, expectedAffinityAssistantName := range tc.expectedAffinityAssistantNames {
684+
ss := &appsv1.StatefulSet{
685+
TypeMeta: metav1.TypeMeta{
686+
Kind: "StatefulSet",
687+
APIVersion: "apps/v1",
688+
},
689+
ObjectMeta: metav1.ObjectMeta{
690+
Name: expectedAffinityAssistantName,
691+
Labels: getStatefulSetLabels(pr, expectedAffinityAssistantName),
692+
},
693+
Status: appsv1.StatefulSetStatus{
694+
ReadyReplicas: 1,
695+
},
696+
}
697+
statefulsets = append(statefulsets, ss)
698+
}
681699

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-
}
700+
var pvcs []*corev1.PersistentVolumeClaim
701+
for _, expectedPVCName := range tc.expectedPVCNames {
702+
pvc := &corev1.PersistentVolumeClaim{
703+
ObjectMeta: metav1.ObjectMeta{
704+
Name: expectedPVCName,
705+
},
706+
}
707+
pvcs = append(pvcs, pvc)
708+
}
709+
710+
data := Data{
711+
StatefulSets: statefulsets,
712+
PVCs: pvcs,
713+
}
714+
715+
_, c, _ := seedTestData(data)
716+
ctx := cfgtesting.SetFeatureFlags(context.Background(), t, tc.cfgMap)
717+
718+
// call clean up
719+
err := c.cleanupAffinityAssistantsAndPVCs(ctx, pr)
720+
if err != nil {
721+
t.Fatalf("unexpected err when clean up Affinity Assistant: %v", err)
722+
}
723+
724+
// validate the cleanup result
725+
for _, expectedAffinityAssistantName := range tc.expectedAffinityAssistantNames {
726+
_, err = c.KubeClientSet.AppsV1().StatefulSets(pr.Namespace).Get(ctx, expectedAffinityAssistantName, metav1.GetOptions{})
727+
if !apierrors.IsNotFound(err) {
728+
t.Errorf("expected a NotFound response of StatefulSet, got: %v", err)
729+
}
730+
}
687731

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)
732+
// validate pvcs
733+
for _, expectedPVCName := range tc.expectedPVCNames {
734+
if tc.aaBehavior == aa.AffinityAssistantPerWorkspace {
735+
// the PVCs are NOT expected to be deleted at Affinity Assistant cleanup time in AffinityAssistantPerWorkspace mode
736+
_, err = c.KubeClientSet.CoreV1().PersistentVolumeClaims(pr.Namespace).Get(ctx, expectedPVCName, metav1.GetOptions{})
737+
if err != nil {
738+
t.Errorf("unexpected err when getting PersistentVolumeClaims, err: %v", err)
739+
}
740+
} else {
741+
_, err = c.KubeClientSet.CoreV1().PersistentVolumeClaims(pr.Namespace).Get(ctx, expectedPVCName, metav1.GetOptions{})
742+
if !apierrors.IsNotFound(err) {
743+
t.Errorf("failed to clean up PersistentVolumeClaim")
744+
}
745+
}
746+
}
692747
}
693748
}
694749

695-
func TestCleanupAffinityAssistants_Failure(t *testing.T) {
750+
func TestCleanupAffinityAssistantsAndPVCs_Failure(t *testing.T) {
696751
pr := &v1.PipelineRun{
697752
Spec: v1.PipelineRunSpec{
698753
Workspaces: []v1.WorkspaceBinding{{
@@ -716,7 +771,7 @@ func TestCleanupAffinityAssistants_Failure(t *testing.T) {
716771
errors.New("failed to delete StatefulSet affinity-assistant-e3b0c44298: error deleting statefulsets"),
717772
})
718773

719-
errs := c.cleanupAffinityAssistants(ctx, pr)
774+
errs := c.cleanupAffinityAssistantsAndPVCs(ctx, pr)
720775
if errs == nil {
721776
t.Fatalf("expecting Affinity Assistant cleanup error but got nil")
722777
}
@@ -747,7 +802,7 @@ func TestThatCleanupIsAvoidedIfAssistantIsDisabled(t *testing.T) {
747802
store := config.NewStore(logtesting.TestLogger(t))
748803
store.OnConfigChanged(configMap)
749804

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

752807
if len(fakeClientSet.Actions()) != 0 {
753808
t.Errorf("Expected 0 k8s client requests, did %d request", len(fakeClientSet.Actions()))
@@ -958,8 +1013,10 @@ func seedTestData(d Data) (context.Context, Reconciler, func()) {
9581013
ctx := context.Background()
9591014
ctx, cancel := context.WithCancel(ctx)
9601015

1016+
kubeClientSet := fakek8s.NewSimpleClientset()
9611017
c := Reconciler{
962-
KubeClientSet: fakek8s.NewSimpleClientset(),
1018+
KubeClientSet: kubeClientSet,
1019+
pvcHandler: volumeclaim.NewPVCHandler(kubeClientSet, zap.NewExample().Sugar()),
9631020
}
9641021
for _, s := range d.StatefulSets {
9651022
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: 47 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,49 @@ func (c *defaultPVCHandler) CreatePVCsForWorkspaces(ctx context.Context, wb []v1
8185
return errorutils.NewAggregate(errs)
8286
}
8387

88+
// PurgeFinalizerAndDeletePVCForWorkspace purges the `kubernetes.io/pvc-protection` finalizer protection of the given pvc and then deletes it.
89+
// Pruging the `kubernetes.io/pvc-protection` finalizer allows the pvc to be deleted even when it is referenced by a taskrun pod.
90+
// See mode details in https://kubernetes.io/docs/concepts/storage/persistent-volumes/#storage-object-in-use-protection.
91+
func (c *defaultPVCHandler) PurgeFinalizerAndDeletePVCForWorkspace(ctx context.Context, pvcName, namespace string) error {
92+
p, err := c.clientset.CoreV1().PersistentVolumeClaims(namespace).Get(ctx, pvcName, metav1.GetOptions{})
93+
if err != nil {
94+
return fmt.Errorf("failed to get the PVC %s: %w", pvcName, err)
95+
}
96+
97+
// get the list of existing finalizers and drop `pvc-protection` if exists
98+
var finalizers []string
99+
for _, f := range p.ObjectMeta.Finalizers {
100+
if f == "kubernetes.io/pvc-protection" {
101+
continue
102+
}
103+
finalizers = append(finalizers, f)
104+
}
105+
106+
// prepare data to remove pvc-protection from the list of finalizers
107+
removeFinalizerBytes, err := json.Marshal([]jsonpatch.JsonPatchOperation{{
108+
Path: "/metadata/finalizers",
109+
Operation: "replace",
110+
Value: finalizers,
111+
}})
112+
if err != nil {
113+
return fmt.Errorf("failed to marshal jsonpatch: %w", err)
114+
}
115+
116+
// patch the existing PVC to update the finalizers
117+
_, err = c.clientset.CoreV1().PersistentVolumeClaims(namespace).Patch(ctx, pvcName, types.JSONPatchType, removeFinalizerBytes, metav1.PatchOptions{})
118+
if err != nil {
119+
return fmt.Errorf("failed to patch the PVC %s: %w", pvcName, err)
120+
}
121+
122+
// delete PVC
123+
err = c.clientset.CoreV1().PersistentVolumeClaims(namespace).Delete(ctx, pvcName, metav1.DeleteOptions{})
124+
if err != nil {
125+
return fmt.Errorf("failed to delete the PVC %s: %w", pvcName, err)
126+
}
127+
128+
return nil
129+
}
130+
84131
func getPVCsWithoutAffinityAssistant(workspaceBindings []v1.WorkspaceBinding, ownerReference metav1.OwnerReference, namespace string) map[string]*corev1.PersistentVolumeClaim {
85132
claims := make(map[string]*corev1.PersistentVolumeClaim)
86133
for _, workspaceBinding := range workspaceBindings {

0 commit comments

Comments
 (0)