1/* 2Copyright 2021 The Kubernetes Authors. 3 4Licensed under the Apache License, Version 2.0 (the "License"); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an "AS IS" BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17package ensurer 18 19import ( 20 "context" 21 "reflect" 22 "testing" 23 24 flowcontrolv1beta1 "k8s.io/api/flowcontrol/v1beta1" 25 apierrors "k8s.io/apimachinery/pkg/api/errors" 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "k8s.io/apiserver/pkg/apis/flowcontrol/bootstrap" 28 "k8s.io/client-go/kubernetes/fake" 29 flowcontrolclient "k8s.io/client-go/kubernetes/typed/flowcontrol/v1beta1" 30 flowcontrolapisv1beta1 "k8s.io/kubernetes/pkg/apis/flowcontrol/v1beta1" 31 32 "github.com/google/go-cmp/cmp" 33 "github.com/stretchr/testify/assert" 34) 35 36func TestEnsureFlowSchema(t *testing.T) { 37 tests := []struct { 38 name string 39 strategy func(flowcontrolclient.FlowSchemaInterface) FlowSchemaEnsurer 40 current *flowcontrolv1beta1.FlowSchema 41 bootstrap *flowcontrolv1beta1.FlowSchema 42 expected *flowcontrolv1beta1.FlowSchema 43 }{ 44 // for suggested configurations 45 { 46 name: "suggested flow schema does not exist - the object should always be re-created", 47 strategy: func(client flowcontrolclient.FlowSchemaInterface) FlowSchemaEnsurer { 48 return NewSuggestedFlowSchemaEnsurer(client) 49 }, 50 bootstrap: newFlowSchema("fs1", "pl1", 100).Object(), 51 current: nil, 52 expected: newFlowSchema("fs1", "pl1", 100).Object(), 53 }, 54 { 55 name: "suggested flow schema exists, auto update is enabled, spec does not match - current object should be updated", 56 strategy: func(client flowcontrolclient.FlowSchemaInterface) FlowSchemaEnsurer { 57 return NewSuggestedFlowSchemaEnsurer(client) 58 }, 59 bootstrap: newFlowSchema("fs1", "pl1", 100).Object(), 60 current: newFlowSchema("fs1", "pl1", 200).WithAutoUpdateAnnotation("true").Object(), 61 expected: newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("true").Object(), 62 }, 63 { 64 name: "suggested flow schema exists, auto update is disabled, spec does not match - current object should not be updated", 65 strategy: func(client flowcontrolclient.FlowSchemaInterface) FlowSchemaEnsurer { 66 return NewSuggestedFlowSchemaEnsurer(client) 67 }, 68 bootstrap: newFlowSchema("fs1", "pl1", 100).Object(), 69 current: newFlowSchema("fs1", "pl1", 200).WithAutoUpdateAnnotation("false").Object(), 70 expected: newFlowSchema("fs1", "pl1", 200).WithAutoUpdateAnnotation("false").Object(), 71 }, 72 73 // for mandatory configurations 74 { 75 name: "mandatory flow schema does not exist - new object should be created", 76 strategy: func(client flowcontrolclient.FlowSchemaInterface) FlowSchemaEnsurer { 77 return NewMandatoryFlowSchemaEnsurer(client) 78 }, 79 bootstrap: newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("true").Object(), 80 current: nil, 81 expected: newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("true").Object(), 82 }, 83 { 84 name: "mandatory flow schema exists, annotation is missing - annotation should be added", 85 strategy: func(client flowcontrolclient.FlowSchemaInterface) FlowSchemaEnsurer { 86 return NewMandatoryFlowSchemaEnsurer(client) 87 }, 88 bootstrap: newFlowSchema("fs1", "pl1", 100).Object(), 89 current: newFlowSchema("fs1", "pl1", 100).Object(), 90 expected: newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("true").Object(), 91 }, 92 { 93 name: "mandatory flow schema exists, auto update is disabled, spec does not match - current object should be updated", 94 strategy: func(client flowcontrolclient.FlowSchemaInterface) FlowSchemaEnsurer { 95 return NewMandatoryFlowSchemaEnsurer(client) 96 }, 97 bootstrap: newFlowSchema("fs1", "pl1", 100).Object(), 98 current: newFlowSchema("fs1", "pl1", 200).WithAutoUpdateAnnotation("false").Object(), 99 expected: newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("true").Object(), 100 }, 101 } 102 103 for _, test := range tests { 104 t.Run(test.name, func(t *testing.T) { 105 client := fake.NewSimpleClientset().FlowcontrolV1beta1().FlowSchemas() 106 if test.current != nil { 107 client.Create(context.TODO(), test.current, metav1.CreateOptions{}) 108 } 109 110 ensurer := test.strategy(client) 111 112 err := ensurer.Ensure([]*flowcontrolv1beta1.FlowSchema{test.bootstrap}) 113 if err != nil { 114 t.Fatalf("Expected no error, but got: %v", err) 115 } 116 117 fsGot, err := client.Get(context.TODO(), test.bootstrap.Name, metav1.GetOptions{}) 118 switch { 119 case test.expected == nil: 120 if !apierrors.IsNotFound(err) { 121 t.Fatalf("Expected GET to return an %q error, but got: %v", metav1.StatusReasonNotFound, err) 122 } 123 case err != nil: 124 t.Fatalf("Expected GET to return no error, but got: %v", err) 125 } 126 127 if !reflect.DeepEqual(test.expected, fsGot) { 128 t.Errorf("FlowSchema does not match - diff: %s", cmp.Diff(test.expected, fsGot)) 129 } 130 }) 131 } 132} 133 134func TestSuggestedFSEnsureStrategy_ShouldUpdate(t *testing.T) { 135 tests := []struct { 136 name string 137 current *flowcontrolv1beta1.FlowSchema 138 bootstrap *flowcontrolv1beta1.FlowSchema 139 newObjectExpected *flowcontrolv1beta1.FlowSchema 140 }{ 141 { 142 name: "auto update is enabled, first generation, spec does not match - spec update expected", 143 current: newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("true").WithGeneration(1).Object(), 144 bootstrap: newFlowSchema("fs1", "pl1", 200).Object(), 145 newObjectExpected: newFlowSchema("fs1", "pl1", 200).WithAutoUpdateAnnotation("true").WithGeneration(1).Object(), 146 }, 147 { 148 name: "auto update is enabled, first generation, spec matches - no update expected", 149 current: newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("true").WithGeneration(1).Object(), 150 bootstrap: newFlowSchema("fs1", "pl1", 100).Object(), 151 newObjectExpected: nil, 152 }, 153 { 154 name: "auto update is enabled, second generation, spec does not match - spec update expected", 155 current: newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("true").WithGeneration(2).Object(), 156 bootstrap: newFlowSchema("fs1", "pl2", 200).Object(), 157 newObjectExpected: newFlowSchema("fs1", "pl2", 200).WithAutoUpdateAnnotation("true").WithGeneration(2).Object(), 158 }, 159 { 160 name: "auto update is enabled, second generation, spec matches - no update expected", 161 current: newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("true").WithGeneration(2).Object(), 162 bootstrap: newFlowSchema("fs1", "pl1", 100).Object(), 163 newObjectExpected: nil, 164 }, 165 { 166 name: "auto update is disabled, first generation, spec does not match - no update expected", 167 current: newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("false").WithGeneration(1).Object(), 168 bootstrap: newFlowSchema("fs1", "pl1", 200).Object(), 169 newObjectExpected: nil, 170 }, 171 { 172 name: "auto update is disabled, first generation, spec matches - no update expected", 173 current: newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("false").WithGeneration(1).Object(), 174 bootstrap: newFlowSchema("fs1", "pl1", 100).Object(), 175 newObjectExpected: nil, 176 }, 177 { 178 name: "auto update is disabled, second generation, spec does not match - no update expected", 179 current: newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("false").WithGeneration(2).Object(), 180 bootstrap: newFlowSchema("fs1", "pl2", 200).Object(), 181 newObjectExpected: nil, 182 }, 183 { 184 name: "auto update is disabled, second generation, spec matches - no update expected", 185 current: newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("false").WithGeneration(2).Object(), 186 bootstrap: newFlowSchema("fs1", "pl1", 100).Object(), 187 newObjectExpected: nil, 188 }, 189 { 190 name: "annotation is missing, first generation, spec does not match - both annotation and spec update expected", 191 current: newFlowSchema("fs1", "pl1", 100).WithGeneration(1).Object(), 192 bootstrap: newFlowSchema("fs1", "pl2", 200).Object(), 193 newObjectExpected: newFlowSchema("fs1", "pl2", 200).WithAutoUpdateAnnotation("true").WithGeneration(1).Object(), 194 }, 195 { 196 name: "annotation is missing, first generation, spec matches - annotation update is expected", 197 current: newFlowSchema("fs1", "pl1", 100).WithGeneration(1).Object(), 198 bootstrap: newFlowSchema("fs1", "pl1", 100).Object(), 199 newObjectExpected: newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("true").WithGeneration(1).Object(), 200 }, 201 { 202 name: "annotation is missing, second generation, spec does not match - annotation update is expected", 203 current: newFlowSchema("fs1", "pl1", 100).WithGeneration(2).Object(), 204 bootstrap: newFlowSchema("fs1", "pl2", 200).Object(), 205 newObjectExpected: newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("false").WithGeneration(2).Object(), 206 }, 207 { 208 name: "annotation is missing, second generation, spec matches - annotation update is expected", 209 current: newFlowSchema("fs1", "pl1", 100).WithGeneration(2).Object(), 210 bootstrap: newFlowSchema("fs1", "pl1", 100).Object(), 211 newObjectExpected: newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("false").WithGeneration(2).Object(), 212 }, 213 } 214 215 for _, test := range tests { 216 t.Run(test.name, func(t *testing.T) { 217 strategy := newSuggestedEnsureStrategy(&flowSchemaWrapper{}) 218 newObjectGot, updateGot, err := strategy.ShouldUpdate(test.current, test.bootstrap) 219 if err != nil { 220 t.Errorf("Expected no error, but got: %v", err) 221 } 222 223 if test.newObjectExpected == nil { 224 if newObjectGot != nil { 225 t.Errorf("Expected a nil object, but got: %#v", newObjectGot) 226 } 227 if updateGot { 228 t.Errorf("Expected update=%t but got: %t", false, updateGot) 229 } 230 return 231 } 232 233 if !updateGot { 234 t.Errorf("Expected update=%t but got: %t", true, updateGot) 235 } 236 if !reflect.DeepEqual(test.newObjectExpected, newObjectGot) { 237 t.Errorf("Expected the object to be updated to match - diff: %s", cmp.Diff(test.newObjectExpected, newObjectGot)) 238 } 239 }) 240 } 241} 242 243func TestFlowSchemaSpecChanged(t *testing.T) { 244 fs1 := &flowcontrolv1beta1.FlowSchema{ 245 Spec: flowcontrolv1beta1.FlowSchemaSpec{}, 246 } 247 fs2 := &flowcontrolv1beta1.FlowSchema{ 248 Spec: flowcontrolv1beta1.FlowSchemaSpec{ 249 MatchingPrecedence: 1, 250 }, 251 } 252 fs1Defaulted := &flowcontrolv1beta1.FlowSchema{ 253 Spec: flowcontrolv1beta1.FlowSchemaSpec{ 254 MatchingPrecedence: flowcontrolapisv1beta1.FlowSchemaDefaultMatchingPrecedence, 255 }, 256 } 257 testCases := []struct { 258 name string 259 expected *flowcontrolv1beta1.FlowSchema 260 actual *flowcontrolv1beta1.FlowSchema 261 specChanged bool 262 }{ 263 { 264 name: "identical flow-schemas should work", 265 expected: bootstrap.MandatoryFlowSchemaCatchAll, 266 actual: bootstrap.MandatoryFlowSchemaCatchAll, 267 specChanged: false, 268 }, 269 { 270 name: "defaulted flow-schemas should work", 271 expected: fs1, 272 actual: fs1Defaulted, 273 specChanged: false, 274 }, 275 { 276 name: "non-defaulted flow-schema has wrong spec", 277 expected: fs1, 278 actual: fs2, 279 specChanged: true, 280 }, 281 } 282 for _, testCase := range testCases { 283 t.Run(testCase.name, func(t *testing.T) { 284 w := flowSchemaSpecChanged(testCase.expected, testCase.actual) 285 assert.Equal(t, testCase.specChanged, w) 286 }) 287 } 288} 289 290func TestRemoveFlowSchema(t *testing.T) { 291 tests := []struct { 292 name string 293 current *flowcontrolv1beta1.FlowSchema 294 bootstrapName string 295 removeExpected bool 296 }{ 297 { 298 name: "flow schema does not exist", 299 bootstrapName: "fs1", 300 current: nil, 301 }, 302 { 303 name: "flow schema exists, auto update is enabled", 304 bootstrapName: "fs1", 305 current: newFlowSchema("fs1", "pl1", 200).WithAutoUpdateAnnotation("true").Object(), 306 removeExpected: true, 307 }, 308 { 309 name: "flow schema exists, auto update is disabled", 310 bootstrapName: "fs1", 311 current: newFlowSchema("fs1", "pl1", 200).WithAutoUpdateAnnotation("false").Object(), 312 removeExpected: false, 313 }, 314 { 315 name: "flow schema exists, the auto-update annotation is malformed", 316 bootstrapName: "fs1", 317 current: newFlowSchema("fs1", "pl1", 200).WithAutoUpdateAnnotation("invalid").Object(), 318 removeExpected: false, 319 }, 320 } 321 322 for _, test := range tests { 323 t.Run(test.name, func(t *testing.T) { 324 client := fake.NewSimpleClientset().FlowcontrolV1beta1().FlowSchemas() 325 if test.current != nil { 326 client.Create(context.TODO(), test.current, metav1.CreateOptions{}) 327 } 328 329 remover := NewFlowSchemaRemover(client) 330 err := remover.Remove([]string{test.bootstrapName}) 331 if err != nil { 332 t.Fatalf("Expected no error, but got: %v", err) 333 } 334 335 if test.current == nil { 336 return 337 } 338 _, err = client.Get(context.TODO(), test.bootstrapName, metav1.GetOptions{}) 339 switch { 340 case test.removeExpected: 341 if !apierrors.IsNotFound(err) { 342 t.Errorf("Expected error: %q, but got: %v", metav1.StatusReasonNotFound, err) 343 } 344 default: 345 if err != nil { 346 t.Errorf("Expected no error, but got: %v", err) 347 } 348 } 349 }) 350 } 351} 352 353func TestGetFlowSchemaRemoveCandidate(t *testing.T) { 354 tests := []struct { 355 name string 356 current []*flowcontrolv1beta1.FlowSchema 357 bootstrap []*flowcontrolv1beta1.FlowSchema 358 expected []string 359 }{ 360 { 361 name: "no object has been removed from the bootstrap configuration", 362 bootstrap: []*flowcontrolv1beta1.FlowSchema{ 363 newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("true").Object(), 364 newFlowSchema("fs2", "pl2", 200).WithAutoUpdateAnnotation("true").Object(), 365 newFlowSchema("fs3", "pl3", 300).WithAutoUpdateAnnotation("true").Object(), 366 }, 367 current: []*flowcontrolv1beta1.FlowSchema{ 368 newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("true").Object(), 369 newFlowSchema("fs2", "pl2", 200).WithAutoUpdateAnnotation("true").Object(), 370 newFlowSchema("fs3", "pl3", 300).WithAutoUpdateAnnotation("true").Object(), 371 }, 372 expected: []string{}, 373 }, 374 { 375 name: "bootstrap is empty, all current objects with the annotation should be candidates", 376 bootstrap: []*flowcontrolv1beta1.FlowSchema{}, 377 current: []*flowcontrolv1beta1.FlowSchema{ 378 newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("true").Object(), 379 newFlowSchema("fs2", "pl2", 200).WithAutoUpdateAnnotation("true").Object(), 380 newFlowSchema("fs3", "pl3", 300).Object(), 381 }, 382 expected: []string{"fs1", "fs2"}, 383 }, 384 { 385 name: "object(s) have been removed from the bootstrap configuration", 386 bootstrap: []*flowcontrolv1beta1.FlowSchema{ 387 newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("true").Object(), 388 }, 389 current: []*flowcontrolv1beta1.FlowSchema{ 390 newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("true").Object(), 391 newFlowSchema("fs2", "pl2", 200).WithAutoUpdateAnnotation("true").Object(), 392 newFlowSchema("fs3", "pl3", 300).WithAutoUpdateAnnotation("true").Object(), 393 }, 394 expected: []string{"fs2", "fs3"}, 395 }, 396 { 397 name: "object(s) without the annotation key are ignored", 398 bootstrap: []*flowcontrolv1beta1.FlowSchema{ 399 newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("true").Object(), 400 }, 401 current: []*flowcontrolv1beta1.FlowSchema{ 402 newFlowSchema("fs1", "pl1", 100).WithAutoUpdateAnnotation("true").Object(), 403 newFlowSchema("fs2", "pl2", 200).Object(), 404 newFlowSchema("fs3", "pl3", 300).Object(), 405 }, 406 expected: []string{}, 407 }, 408 } 409 410 for _, test := range tests { 411 t.Run(test.name, func(t *testing.T) { 412 client := fake.NewSimpleClientset().FlowcontrolV1beta1().FlowSchemas() 413 for i := range test.current { 414 client.Create(context.TODO(), test.current[i], metav1.CreateOptions{}) 415 } 416 417 removeListGot, err := GetFlowSchemaRemoveCandidate(client, test.bootstrap) 418 if err != nil { 419 t.Fatalf("Expected no error, but got: %v", err) 420 } 421 422 if !cmp.Equal(test.expected, removeListGot) { 423 t.Errorf("Remove candidate list does not match - diff: %s", cmp.Diff(test.expected, removeListGot)) 424 } 425 }) 426 } 427} 428 429type fsBuilder struct { 430 object *flowcontrolv1beta1.FlowSchema 431} 432 433func newFlowSchema(name, plName string, matchingPrecedence int32) *fsBuilder { 434 return &fsBuilder{ 435 object: &flowcontrolv1beta1.FlowSchema{ 436 ObjectMeta: metav1.ObjectMeta{ 437 Name: name, 438 }, 439 Spec: flowcontrolv1beta1.FlowSchemaSpec{ 440 PriorityLevelConfiguration: flowcontrolv1beta1.PriorityLevelConfigurationReference{ 441 Name: plName, 442 }, 443 MatchingPrecedence: matchingPrecedence, 444 }, 445 }, 446 } 447} 448 449func (b *fsBuilder) Object() *flowcontrolv1beta1.FlowSchema { 450 return b.object 451} 452 453func (b *fsBuilder) WithGeneration(value int64) *fsBuilder { 454 b.object.SetGeneration(value) 455 return b 456} 457 458func (b *fsBuilder) WithAutoUpdateAnnotation(value string) *fsBuilder { 459 setAnnotation(b.object, value) 460 return b 461} 462 463func setAnnotation(accessor metav1.Object, value string) { 464 if accessor.GetAnnotations() == nil { 465 accessor.SetAnnotations(map[string]string{}) 466 } 467 468 accessor.GetAnnotations()[flowcontrolv1beta1.AutoUpdateAnnotationKey] = value 469} 470