1
16
17 package poddisruptionbudget
18
19 import (
20 "reflect"
21 "testing"
22
23 "github.com/google/go-cmp/cmp"
24
25 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26 "k8s.io/apimachinery/pkg/util/intstr"
27 genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
28 utilfeature "k8s.io/apiserver/pkg/util/feature"
29 featuregatetesting "k8s.io/component-base/featuregate/testing"
30 "k8s.io/kubernetes/pkg/apis/policy"
31 "k8s.io/kubernetes/pkg/features"
32 )
33
34 type unhealthyPodEvictionPolicyStrategyTestCase struct {
35 name string
36 enableUnhealthyPodEvictionPolicy bool
37 disablePDBUnhealthyPodEvictionPolicyFeatureGateAfterCreate bool
38 unhealthyPodEvictionPolicy *policy.UnhealthyPodEvictionPolicyType
39 expectedUnhealthyPodEvictionPolicy *policy.UnhealthyPodEvictionPolicyType
40 expectedValidationErr bool
41 updateUnhealthyPodEvictionPolicy *policy.UnhealthyPodEvictionPolicyType
42 expectedUpdateUnhealthyPodEvictionPolicy *policy.UnhealthyPodEvictionPolicyType
43 expectedValidationUpdateErr bool
44 }
45
46 func TestPodDisruptionBudgetStrategy(t *testing.T) {
47 tests := map[string]bool{
48 "PodDisruptionBudget strategy with PDBUnhealthyPodEvictionPolicy feature gate disabled": false,
49 "PodDisruptionBudget strategy with PDBUnhealthyPodEvictionPolicy feature gate enabled": true,
50 }
51
52 for name, enableUnhealthyPodEvictionPolicy := range tests {
53 t.Run(name, func(t *testing.T) {
54 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PDBUnhealthyPodEvictionPolicy, enableUnhealthyPodEvictionPolicy)()
55 testPodDisruptionBudgetStrategy(t)
56 })
57 }
58
59 healthyPolicyTests := []unhealthyPodEvictionPolicyStrategyTestCase{
60 {
61 name: "PodDisruptionBudget strategy with FeatureGate disabled should remove unhealthyPodEvictionPolicy",
62 enableUnhealthyPodEvictionPolicy: false,
63 unhealthyPodEvictionPolicy: unhealthyPolicyPtr(policy.IfHealthyBudget),
64 updateUnhealthyPodEvictionPolicy: unhealthyPolicyPtr(policy.IfHealthyBudget),
65 },
66 {
67 name: "PodDisruptionBudget strategy with FeatureGate disabled should remove invalid unhealthyPodEvictionPolicy",
68 enableUnhealthyPodEvictionPolicy: false,
69 unhealthyPodEvictionPolicy: unhealthyPolicyPtr("Invalid"),
70 updateUnhealthyPodEvictionPolicy: unhealthyPolicyPtr("Invalid"),
71 },
72 {
73 name: "PodDisruptionBudget strategy with FeatureGate enabled",
74 enableUnhealthyPodEvictionPolicy: true,
75 },
76 {
77 name: "PodDisruptionBudget strategy with FeatureGate enabled should respect unhealthyPodEvictionPolicy",
78 enableUnhealthyPodEvictionPolicy: true,
79 unhealthyPodEvictionPolicy: unhealthyPolicyPtr(policy.AlwaysAllow),
80 expectedUnhealthyPodEvictionPolicy: unhealthyPolicyPtr(policy.AlwaysAllow),
81 updateUnhealthyPodEvictionPolicy: unhealthyPolicyPtr(policy.IfHealthyBudget),
82 expectedUpdateUnhealthyPodEvictionPolicy: unhealthyPolicyPtr(policy.IfHealthyBudget),
83 },
84 {
85 name: "PodDisruptionBudget strategy with FeatureGate enabled should fail invalid unhealthyPodEvictionPolicy",
86 enableUnhealthyPodEvictionPolicy: true,
87 unhealthyPodEvictionPolicy: unhealthyPolicyPtr("Invalid"),
88 expectedValidationErr: true,
89 },
90 {
91 name: "PodDisruptionBudget strategy with FeatureGate enabled should fail invalid unhealthyPodEvictionPolicy when updated",
92 enableUnhealthyPodEvictionPolicy: true,
93 updateUnhealthyPodEvictionPolicy: unhealthyPolicyPtr("Invalid"),
94 expectedValidationUpdateErr: true,
95 },
96 {
97 name: "PodDisruptionBudget strategy with unhealthyPodEvictionPolicy should be updated when feature gate is disabled",
98 enableUnhealthyPodEvictionPolicy: true,
99 disablePDBUnhealthyPodEvictionPolicyFeatureGateAfterCreate: true,
100 unhealthyPodEvictionPolicy: unhealthyPolicyPtr(policy.AlwaysAllow),
101 expectedUnhealthyPodEvictionPolicy: unhealthyPolicyPtr(policy.AlwaysAllow),
102 updateUnhealthyPodEvictionPolicy: unhealthyPolicyPtr(policy.IfHealthyBudget),
103 expectedUpdateUnhealthyPodEvictionPolicy: unhealthyPolicyPtr(policy.IfHealthyBudget),
104 },
105 {
106 name: "PodDisruptionBudget strategy with unhealthyPodEvictionPolicy should not be updated to invalid when feature gate is disabled",
107 enableUnhealthyPodEvictionPolicy: true,
108 disablePDBUnhealthyPodEvictionPolicyFeatureGateAfterCreate: true,
109 unhealthyPodEvictionPolicy: unhealthyPolicyPtr(policy.AlwaysAllow),
110 expectedUnhealthyPodEvictionPolicy: unhealthyPolicyPtr(policy.AlwaysAllow),
111 updateUnhealthyPodEvictionPolicy: unhealthyPolicyPtr("Invalid"),
112 expectedValidationUpdateErr: true,
113 expectedUpdateUnhealthyPodEvictionPolicy: unhealthyPolicyPtr(policy.AlwaysAllow),
114 },
115 }
116
117 for _, tc := range healthyPolicyTests {
118 t.Run(tc.name, func(t *testing.T) {
119 testPodDisruptionBudgetStrategyWithUnhealthyPodEvictionPolicy(t, tc)
120 })
121 }
122 }
123
124 func testPodDisruptionBudgetStrategyWithUnhealthyPodEvictionPolicy(t *testing.T, tc unhealthyPodEvictionPolicyStrategyTestCase) {
125 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PDBUnhealthyPodEvictionPolicy, tc.enableUnhealthyPodEvictionPolicy)()
126 ctx := genericapirequest.NewDefaultContext()
127 if !Strategy.NamespaceScoped() {
128 t.Errorf("PodDisruptionBudget must be namespace scoped")
129 }
130 if Strategy.AllowCreateOnUpdate() {
131 t.Errorf("PodDisruptionBudget should not allow create on update")
132 }
133
134 validSelector := map[string]string{"a": "b"}
135 minAvailable := intstr.FromInt32(3)
136 pdb := &policy.PodDisruptionBudget{
137 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
138 Spec: policy.PodDisruptionBudgetSpec{
139 MinAvailable: &minAvailable,
140 Selector: &metav1.LabelSelector{MatchLabels: validSelector},
141 UnhealthyPodEvictionPolicy: tc.unhealthyPodEvictionPolicy,
142 },
143 }
144
145 Strategy.PrepareForCreate(ctx, pdb)
146 errs := Strategy.Validate(ctx, pdb)
147 if len(errs) != 0 {
148 if !tc.expectedValidationErr {
149 t.Errorf("Unexpected error validating %v", errs)
150 }
151 return
152 }
153 if len(errs) == 0 && tc.expectedValidationErr {
154 t.Errorf("Expected error validating")
155 }
156 if !reflect.DeepEqual(pdb.Spec.UnhealthyPodEvictionPolicy, tc.expectedUnhealthyPodEvictionPolicy) {
157 t.Errorf("Unexpected UnhealthyPodEvictionPolicy set: expected %v, got %v", tc.expectedUnhealthyPodEvictionPolicy, pdb.Spec.UnhealthyPodEvictionPolicy)
158 }
159 if tc.disablePDBUnhealthyPodEvictionPolicyFeatureGateAfterCreate {
160 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PDBUnhealthyPodEvictionPolicy, false)()
161 }
162
163 newPdb := &policy.PodDisruptionBudget{
164 ObjectMeta: metav1.ObjectMeta{Name: pdb.Name, Namespace: pdb.Namespace},
165 Spec: pdb.Spec,
166 }
167 if tc.updateUnhealthyPodEvictionPolicy != nil {
168 newPdb.Spec.UnhealthyPodEvictionPolicy = tc.updateUnhealthyPodEvictionPolicy
169 }
170
171
172 Strategy.PrepareForUpdate(ctx, newPdb, pdb)
173 errs = Strategy.ValidateUpdate(ctx, newPdb, pdb)
174
175 if len(errs) != 0 {
176 if !tc.expectedValidationUpdateErr {
177 t.Errorf("Unexpected error updating PodDisruptionBudget %v", errs)
178 }
179 return
180 }
181 if len(errs) == 0 && tc.expectedValidationUpdateErr {
182 t.Errorf("Expected error updating PodDisruptionBudget")
183 }
184 if !reflect.DeepEqual(newPdb.Spec.UnhealthyPodEvictionPolicy, tc.expectedUpdateUnhealthyPodEvictionPolicy) {
185 t.Errorf("Unexpected UnhealthyPodEvictionPolicy set: expected %v, got %v", tc.expectedUpdateUnhealthyPodEvictionPolicy, newPdb.Spec.UnhealthyPodEvictionPolicy)
186 }
187 }
188
189 func testPodDisruptionBudgetStrategy(t *testing.T) {
190 ctx := genericapirequest.NewDefaultContext()
191 if !Strategy.NamespaceScoped() {
192 t.Errorf("PodDisruptionBudget must be namespace scoped")
193 }
194 if Strategy.AllowCreateOnUpdate() {
195 t.Errorf("PodDisruptionBudget should not allow create on update")
196 }
197
198 validSelector := map[string]string{"a": "b"}
199 minAvailable := intstr.FromInt32(3)
200 pdb := &policy.PodDisruptionBudget{
201 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
202 Spec: policy.PodDisruptionBudgetSpec{
203 MinAvailable: &minAvailable,
204 Selector: &metav1.LabelSelector{MatchLabels: validSelector},
205 },
206 }
207
208 Strategy.PrepareForCreate(ctx, pdb)
209 errs := Strategy.Validate(ctx, pdb)
210 if len(errs) != 0 {
211 t.Errorf("Unexpected error validating %v", errs)
212 }
213
214 newPdb := &policy.PodDisruptionBudget{
215 ObjectMeta: metav1.ObjectMeta{Name: pdb.Name, Namespace: pdb.Namespace},
216 Spec: pdb.Spec,
217 Status: policy.PodDisruptionBudgetStatus{
218 DisruptionsAllowed: 1,
219 CurrentHealthy: 3,
220 DesiredHealthy: 3,
221 ExpectedPods: 3,
222 },
223 }
224
225
226 Strategy.PrepareForUpdate(ctx, newPdb, pdb)
227 errs = Strategy.ValidateUpdate(ctx, newPdb, pdb)
228 if len(errs) != 0 {
229 t.Errorf("Unexpected error updating PodDisruptionBudget.")
230 }
231
232
233 newPdb.Spec.Selector = &metav1.LabelSelector{MatchLabels: map[string]string{"a": "bar"}}
234 Strategy.PrepareForUpdate(ctx, newPdb, pdb)
235 errs = Strategy.ValidateUpdate(ctx, newPdb, pdb)
236 if len(errs) != 0 {
237 t.Errorf("Expected no error on changing selector on poddisruptionbudgets.")
238 }
239 newPdb.Spec.Selector = pdb.Spec.Selector
240
241
242 newMinAvailable := intstr.FromString("28%")
243 newPdb.Spec.MinAvailable = &newMinAvailable
244 Strategy.PrepareForUpdate(ctx, newPdb, pdb)
245 errs = Strategy.ValidateUpdate(ctx, newPdb, pdb)
246 if len(errs) != 0 {
247 t.Errorf("Expected no error updating MinAvailable on poddisruptionbudgets.")
248 }
249
250
251 maxUnavailable := intstr.FromString("28%")
252 newPdb.Spec.MaxUnavailable = &maxUnavailable
253 newPdb.Spec.MinAvailable = nil
254 Strategy.PrepareForUpdate(ctx, newPdb, pdb)
255 errs = Strategy.ValidateUpdate(ctx, newPdb, pdb)
256 if len(errs) != 0 {
257 t.Errorf("Expected no error updating replacing MinAvailable with MaxUnavailable on poddisruptionbudgets.")
258 }
259 }
260
261 func TestPodDisruptionBudgetStatusStrategy(t *testing.T) {
262 ctx := genericapirequest.NewDefaultContext()
263 if !StatusStrategy.NamespaceScoped() {
264 t.Errorf("PodDisruptionBudgetStatus must be namespace scoped")
265 }
266 if StatusStrategy.AllowCreateOnUpdate() {
267 t.Errorf("PodDisruptionBudgetStatus should not allow create on update")
268 }
269
270 oldMinAvailable := intstr.FromInt32(3)
271 newMinAvailable := intstr.FromInt32(2)
272
273 validSelector := map[string]string{"a": "b"}
274 oldPdb := &policy.PodDisruptionBudget{
275 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault, ResourceVersion: "10"},
276 Spec: policy.PodDisruptionBudgetSpec{
277 Selector: &metav1.LabelSelector{MatchLabels: validSelector},
278 MinAvailable: &oldMinAvailable,
279 },
280 Status: policy.PodDisruptionBudgetStatus{
281 DisruptionsAllowed: 1,
282 CurrentHealthy: 3,
283 DesiredHealthy: 3,
284 ExpectedPods: 3,
285 },
286 }
287 newPdb := &policy.PodDisruptionBudget{
288 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault, ResourceVersion: "9"},
289 Spec: policy.PodDisruptionBudgetSpec{
290 Selector: &metav1.LabelSelector{MatchLabels: validSelector},
291 MinAvailable: &newMinAvailable,
292 },
293 Status: policy.PodDisruptionBudgetStatus{
294 DisruptionsAllowed: 0,
295 CurrentHealthy: 2,
296 DesiredHealthy: 3,
297 ExpectedPods: 3,
298 },
299 }
300 StatusStrategy.PrepareForUpdate(ctx, newPdb, oldPdb)
301 if newPdb.Status.CurrentHealthy != 2 {
302 t.Errorf("PodDisruptionBudget status updates should allow change of CurrentHealthy: %v", newPdb.Status.CurrentHealthy)
303 }
304 if newPdb.Spec.MinAvailable.IntValue() != 3 {
305 t.Errorf("PodDisruptionBudget status updates should not clobber spec: %v", newPdb.Spec)
306 }
307 errs := StatusStrategy.ValidateUpdate(ctx, newPdb, oldPdb)
308 if len(errs) != 0 {
309 t.Errorf("Unexpected error %v", errs)
310 }
311 }
312
313 func TestPodDisruptionBudgetStatusValidationByApiVersion(t *testing.T) {
314 testCases := map[string]struct {
315 apiVersion string
316 validation bool
317 }{
318 "policy/v1beta1 should not do update validation": {
319 apiVersion: "v1beta1",
320 validation: false,
321 },
322 "policy/v1 should do update validation": {
323 apiVersion: "v1",
324 validation: true,
325 },
326 "policy/some-version should do update validation": {
327 apiVersion: "some-version",
328 validation: true,
329 },
330 }
331
332 for tn, tc := range testCases {
333 t.Run(tn, func(t *testing.T) {
334 ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(),
335 &genericapirequest.RequestInfo{
336 APIGroup: "policy",
337 APIVersion: tc.apiVersion,
338 })
339
340 oldMaxUnavailable := intstr.FromInt32(2)
341 newMaxUnavailable := intstr.FromInt32(3)
342 oldPdb := &policy.PodDisruptionBudget{
343 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault, ResourceVersion: "10"},
344 Spec: policy.PodDisruptionBudgetSpec{
345 Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"a": "b"}},
346 MaxUnavailable: &oldMaxUnavailable,
347 },
348 Status: policy.PodDisruptionBudgetStatus{
349 DisruptionsAllowed: 1,
350 },
351 }
352 newPdb := &policy.PodDisruptionBudget{
353 ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault, ResourceVersion: "9"},
354 Spec: policy.PodDisruptionBudgetSpec{
355 Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"a": "b"}},
356 MinAvailable: &newMaxUnavailable,
357 },
358 Status: policy.PodDisruptionBudgetStatus{
359 DisruptionsAllowed: -1,
360 },
361 }
362
363 errs := StatusStrategy.ValidateUpdate(ctx, newPdb, oldPdb)
364 hasErrors := len(errs) > 0
365 if !tc.validation && hasErrors {
366 t.Errorf("Validation failed when no validation should happen")
367 }
368 if tc.validation && !hasErrors {
369 t.Errorf("Expected validation errors but didn't get any")
370 }
371 })
372 }
373 }
374
375 func TestDropDisabledFields(t *testing.T) {
376 tests := map[string]struct {
377 oldSpec *policy.PodDisruptionBudgetSpec
378 newSpec *policy.PodDisruptionBudgetSpec
379 expectNewSpec *policy.PodDisruptionBudgetSpec
380 enableUnhealthyPodEvictionPolicy bool
381 }{
382 "disabled clears unhealthyPodEvictionPolicy": {
383 enableUnhealthyPodEvictionPolicy: false,
384 oldSpec: nil,
385 newSpec: specWithUnhealthyPodEvictionPolicy(unhealthyPolicyPtr(policy.IfHealthyBudget)),
386 expectNewSpec: specWithUnhealthyPodEvictionPolicy(nil),
387 },
388 "disabled does not allow updating unhealthyPodEvictionPolicy": {
389 enableUnhealthyPodEvictionPolicy: false,
390 oldSpec: specWithUnhealthyPodEvictionPolicy(nil),
391 newSpec: specWithUnhealthyPodEvictionPolicy(unhealthyPolicyPtr(policy.IfHealthyBudget)),
392 expectNewSpec: specWithUnhealthyPodEvictionPolicy(nil),
393 },
394 "disabled preserves old unhealthyPodEvictionPolicy when both old and new have it": {
395 enableUnhealthyPodEvictionPolicy: false,
396 oldSpec: specWithUnhealthyPodEvictionPolicy(unhealthyPolicyPtr(policy.IfHealthyBudget)),
397 newSpec: specWithUnhealthyPodEvictionPolicy(unhealthyPolicyPtr(policy.IfHealthyBudget)),
398 expectNewSpec: specWithUnhealthyPodEvictionPolicy(unhealthyPolicyPtr(policy.IfHealthyBudget)),
399 },
400 "disabled allows updating unhealthyPodEvictionPolicy": {
401 enableUnhealthyPodEvictionPolicy: false,
402 oldSpec: specWithUnhealthyPodEvictionPolicy(unhealthyPolicyPtr(policy.IfHealthyBudget)),
403 newSpec: specWithUnhealthyPodEvictionPolicy(unhealthyPolicyPtr(policy.AlwaysAllow)),
404 expectNewSpec: specWithUnhealthyPodEvictionPolicy(unhealthyPolicyPtr(policy.AlwaysAllow)),
405 },
406 "enabled preserve unhealthyPodEvictionPolicy": {
407 enableUnhealthyPodEvictionPolicy: true,
408 oldSpec: nil,
409 newSpec: specWithUnhealthyPodEvictionPolicy(unhealthyPolicyPtr(policy.IfHealthyBudget)),
410 expectNewSpec: specWithUnhealthyPodEvictionPolicy(unhealthyPolicyPtr(policy.IfHealthyBudget)),
411 },
412 "enabled allows updating unhealthyPodEvictionPolicy": {
413 enableUnhealthyPodEvictionPolicy: true,
414 oldSpec: specWithUnhealthyPodEvictionPolicy(nil),
415 newSpec: specWithUnhealthyPodEvictionPolicy(unhealthyPolicyPtr(policy.IfHealthyBudget)),
416 expectNewSpec: specWithUnhealthyPodEvictionPolicy(unhealthyPolicyPtr(policy.IfHealthyBudget)),
417 },
418 "enabled preserve unhealthyPodEvictionPolicy when both old and new have it": {
419 enableUnhealthyPodEvictionPolicy: true,
420 oldSpec: specWithUnhealthyPodEvictionPolicy(unhealthyPolicyPtr(policy.IfHealthyBudget)),
421 newSpec: specWithUnhealthyPodEvictionPolicy(unhealthyPolicyPtr(policy.IfHealthyBudget)),
422 expectNewSpec: specWithUnhealthyPodEvictionPolicy(unhealthyPolicyPtr(policy.IfHealthyBudget)),
423 },
424 "enabled updates unhealthyPodEvictionPolicy": {
425 enableUnhealthyPodEvictionPolicy: true,
426 oldSpec: specWithUnhealthyPodEvictionPolicy(unhealthyPolicyPtr(policy.IfHealthyBudget)),
427 newSpec: specWithUnhealthyPodEvictionPolicy(unhealthyPolicyPtr(policy.AlwaysAllow)),
428 expectNewSpec: specWithUnhealthyPodEvictionPolicy(unhealthyPolicyPtr(policy.AlwaysAllow)),
429 },
430 }
431
432 for name, tc := range tests {
433 t.Run(name, func(t *testing.T) {
434 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PDBUnhealthyPodEvictionPolicy, tc.enableUnhealthyPodEvictionPolicy)()
435
436 oldSpecBefore := tc.oldSpec.DeepCopy()
437 dropDisabledFields(tc.newSpec, tc.oldSpec)
438 if !reflect.DeepEqual(tc.newSpec, tc.expectNewSpec) {
439 t.Error(cmp.Diff(tc.newSpec, tc.expectNewSpec))
440 }
441 if !reflect.DeepEqual(tc.oldSpec, oldSpecBefore) {
442 t.Error(cmp.Diff(tc.oldSpec, oldSpecBefore))
443 }
444 })
445 }
446 }
447
448 func unhealthyPolicyPtr(unhealthyPodEvictionPolicy policy.UnhealthyPodEvictionPolicyType) *policy.UnhealthyPodEvictionPolicyType {
449 return &unhealthyPodEvictionPolicy
450 }
451
452 func specWithUnhealthyPodEvictionPolicy(unhealthyPodEvictionPolicy *policy.UnhealthyPodEvictionPolicyType) *policy.PodDisruptionBudgetSpec {
453 return &policy.PodDisruptionBudgetSpec{
454 UnhealthyPodEvictionPolicy: unhealthyPodEvictionPolicy,
455 }
456 }
457
View as plain text