1
16
17 package ephemeral
18
19 import (
20 "context"
21 "errors"
22 "sort"
23 "testing"
24
25 "github.com/stretchr/testify/assert"
26 v1 "k8s.io/api/core/v1"
27 apierrors "k8s.io/apimachinery/pkg/api/errors"
28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29 "k8s.io/apimachinery/pkg/runtime"
30 "k8s.io/apimachinery/pkg/types"
31 "k8s.io/client-go/informers"
32 "k8s.io/client-go/kubernetes/fake"
33 k8stesting "k8s.io/client-go/testing"
34 "k8s.io/client-go/tools/cache"
35 "k8s.io/component-base/metrics/testutil"
36 "k8s.io/klog/v2"
37 "k8s.io/kubernetes/pkg/controller"
38 ephemeralvolumemetrics "k8s.io/kubernetes/pkg/controller/volume/ephemeral/metrics"
39 )
40
41 var (
42 testPodName = "test-pod"
43 testNamespace = "my-namespace"
44 testPodUID = types.UID("uidpod1")
45 otherNamespace = "not-my-namespace"
46 ephemeralVolumeName = "ephemeral-volume"
47
48 testPod = makePod(testPodName, testNamespace, testPodUID)
49 testPodWithEphemeral = makePod(testPodName, testNamespace, testPodUID, *makeEphemeralVolume(ephemeralVolumeName))
50 testPodEphemeralClaim = makePVC(testPodName+"-"+ephemeralVolumeName, testNamespace, makeOwnerReference(testPodWithEphemeral, true))
51 conflictingClaim = makePVC(testPodName+"-"+ephemeralVolumeName, testNamespace, nil)
52 otherNamespaceClaim = makePVC(testPodName+"-"+ephemeralVolumeName, otherNamespace, nil)
53 )
54
55 func init() {
56 klog.InitFlags(nil)
57 }
58
59 func TestSyncHandler(t *testing.T) {
60 tests := []struct {
61 name string
62 podKey string
63 pvcs []*v1.PersistentVolumeClaim
64 pods []*v1.Pod
65 expectedPVCs []v1.PersistentVolumeClaim
66 expectedError bool
67 expectedMetrics expectedMetrics
68 }{
69 {
70 name: "create",
71 pods: []*v1.Pod{testPodWithEphemeral},
72 podKey: podKey(testPodWithEphemeral),
73 expectedPVCs: []v1.PersistentVolumeClaim{*testPodEphemeralClaim},
74 expectedMetrics: expectedMetrics{1, 0},
75 },
76 {
77 name: "no-such-pod",
78 podKey: podKey(testPodWithEphemeral),
79 },
80 {
81 name: "pod-deleted",
82 pods: func() []*v1.Pod {
83 deleted := metav1.Now()
84 pods := []*v1.Pod{testPodWithEphemeral.DeepCopy()}
85 pods[0].DeletionTimestamp = &deleted
86 return pods
87 }(),
88 podKey: podKey(testPodWithEphemeral),
89 },
90 {
91 name: "no-volumes",
92 pods: []*v1.Pod{testPod},
93 podKey: podKey(testPod),
94 },
95 {
96 name: "create-with-other-PVC",
97 pods: []*v1.Pod{testPodWithEphemeral},
98 podKey: podKey(testPodWithEphemeral),
99 pvcs: []*v1.PersistentVolumeClaim{otherNamespaceClaim},
100 expectedPVCs: []v1.PersistentVolumeClaim{*otherNamespaceClaim, *testPodEphemeralClaim},
101 expectedMetrics: expectedMetrics{1, 0},
102 },
103 {
104 name: "wrong-PVC-owner",
105 pods: []*v1.Pod{testPodWithEphemeral},
106 podKey: podKey(testPodWithEphemeral),
107 pvcs: []*v1.PersistentVolumeClaim{conflictingClaim},
108 expectedPVCs: []v1.PersistentVolumeClaim{*conflictingClaim},
109 expectedError: true,
110 },
111 {
112 name: "create-conflict",
113 pods: []*v1.Pod{testPodWithEphemeral},
114 podKey: podKey(testPodWithEphemeral),
115 expectedMetrics: expectedMetrics{1, 1},
116 expectedError: true,
117 },
118 }
119
120 for _, tc := range tests {
121
122 t.Run(tc.name, func(t *testing.T) {
123
124
125
126
127 ctx := context.Background()
128
129 var objects []runtime.Object
130 for _, pod := range tc.pods {
131 objects = append(objects, pod)
132 }
133 for _, pvc := range tc.pvcs {
134 objects = append(objects, pvc)
135 }
136
137 fakeKubeClient := createTestClient(objects...)
138 if tc.expectedMetrics.numFailures > 0 {
139 fakeKubeClient.PrependReactor("create", "persistentvolumeclaims", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
140 return true, nil, apierrors.NewConflict(action.GetResource().GroupResource(), "fake name", errors.New("fake conflict"))
141 })
142 }
143 setupMetrics()
144 informerFactory := informers.NewSharedInformerFactory(fakeKubeClient, controller.NoResyncPeriodFunc())
145 podInformer := informerFactory.Core().V1().Pods()
146 pvcInformer := informerFactory.Core().V1().PersistentVolumeClaims()
147
148 c, err := NewController(ctx, fakeKubeClient, podInformer, pvcInformer)
149 if err != nil {
150 t.Fatalf("error creating ephemeral controller : %v", err)
151 }
152 ec, _ := c.(*ephemeralController)
153
154
155 go informerFactory.Start(ctx.Done())
156 informerFactory.WaitForCacheSync(ctx.Done())
157 cache.WaitForCacheSync(ctx.Done(), podInformer.Informer().HasSynced, pvcInformer.Informer().HasSynced)
158
159 err = ec.syncHandler(context.TODO(), tc.podKey)
160 if err != nil && !tc.expectedError {
161 t.Fatalf("unexpected error while running handler: %v", err)
162 }
163 if err == nil && tc.expectedError {
164 t.Fatalf("unexpected success")
165 }
166
167 pvcs, err := fakeKubeClient.CoreV1().PersistentVolumeClaims("").List(ctx, metav1.ListOptions{})
168 if err != nil {
169 t.Fatalf("unexpected error while listing PVCs: %v", err)
170 }
171 assert.Equal(t, sortPVCs(tc.expectedPVCs), sortPVCs(pvcs.Items))
172 expectMetrics(t, tc.expectedMetrics)
173 })
174 }
175 }
176
177 func makePVC(name, namespace string, owner *metav1.OwnerReference) *v1.PersistentVolumeClaim {
178 pvc := &v1.PersistentVolumeClaim{
179 ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace},
180 Spec: v1.PersistentVolumeClaimSpec{},
181 }
182 if owner != nil {
183 pvc.OwnerReferences = []metav1.OwnerReference{*owner}
184 }
185
186 return pvc
187 }
188
189 func makeEphemeralVolume(name string) *v1.Volume {
190 return &v1.Volume{
191 Name: name,
192 VolumeSource: v1.VolumeSource{
193 Ephemeral: &v1.EphemeralVolumeSource{
194 VolumeClaimTemplate: &v1.PersistentVolumeClaimTemplate{},
195 },
196 },
197 }
198 }
199
200 func makePod(name, namespace string, uid types.UID, volumes ...v1.Volume) *v1.Pod {
201 pvc := &v1.Pod{
202 ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace, UID: uid},
203 Spec: v1.PodSpec{
204 Volumes: volumes,
205 },
206 }
207
208 return pvc
209 }
210
211 func podKey(pod *v1.Pod) string {
212 key, _ := cache.DeletionHandlingMetaNamespaceKeyFunc(testPodWithEphemeral)
213 return key
214 }
215
216 func makeOwnerReference(pod *v1.Pod, isController bool) *metav1.OwnerReference {
217 isTrue := true
218 return &metav1.OwnerReference{
219 APIVersion: "v1",
220 Kind: "Pod",
221 Name: pod.Name,
222 UID: pod.UID,
223 Controller: &isController,
224 BlockOwnerDeletion: &isTrue,
225 }
226 }
227
228 func sortPVCs(pvcs []v1.PersistentVolumeClaim) []v1.PersistentVolumeClaim {
229 sort.Slice(pvcs, func(i, j int) bool {
230 return pvcs[i].Namespace < pvcs[j].Namespace ||
231 pvcs[i].Name < pvcs[j].Name
232 })
233 return pvcs
234 }
235
236 func createTestClient(objects ...runtime.Object) *fake.Clientset {
237 fakeClient := fake.NewSimpleClientset(objects...)
238 return fakeClient
239 }
240
241
242
243 type expectedMetrics struct {
244 numCreated int
245 numFailures int
246 }
247
248 func expectMetrics(t *testing.T, em expectedMetrics) {
249 t.Helper()
250
251 actualCreated, err := testutil.GetCounterMetricValue(ephemeralvolumemetrics.EphemeralVolumeCreateAttempts)
252 handleErr(t, err, "ephemeralVolumeCreate")
253 if actualCreated != float64(em.numCreated) {
254 t.Errorf("Expected PVCs to be created %d, got %v", em.numCreated, actualCreated)
255 }
256 actualConflicts, err := testutil.GetCounterMetricValue(ephemeralvolumemetrics.EphemeralVolumeCreateFailures)
257 handleErr(t, err, "ephemeralVolumeCreate/Conflict")
258 if actualConflicts != float64(em.numFailures) {
259 t.Errorf("Expected PVCs to have conflicts %d, got %v", em.numFailures, actualConflicts)
260 }
261 }
262
263 func handleErr(t *testing.T, err error, metricName string) {
264 if err != nil {
265 t.Errorf("Failed to get %s value, err: %v", metricName, err)
266 }
267 }
268
269 func setupMetrics() {
270 ephemeralvolumemetrics.RegisterMetrics()
271 ephemeralvolumemetrics.EphemeralVolumeCreateAttempts.Reset()
272 ephemeralvolumemetrics.EphemeralVolumeCreateFailures.Reset()
273 }
274
View as plain text