github.com/operator-framework/operator-lifecycle-manager@v0.30.0/pkg/controller/operators/openshift/helpers_test.go (about) 1 package openshift 2 3 import ( 4 "context" 5 "fmt" 6 "testing" 7 8 semver "github.com/blang/semver/v4" 9 configv1 "github.com/openshift/api/config/v1" 10 "github.com/stretchr/testify/require" 11 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 "k8s.io/apimachinery/pkg/labels" 13 "k8s.io/apimachinery/pkg/runtime" 14 "sigs.k8s.io/controller-runtime/pkg/client" 15 "sigs.k8s.io/controller-runtime/pkg/client/fake" 16 17 operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" 18 "github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver/projection" 19 "github.com/operator-framework/operator-registry/pkg/api" 20 ) 21 22 func TestConditionsEqual(t *testing.T) { 23 type args struct { 24 a, b *configv1.ClusterOperatorStatusCondition 25 } 26 for _, tt := range []struct { 27 description string 28 args args 29 expect bool 30 }{ 31 { 32 description: "Nil/Both", 33 expect: true, 34 }, 35 { 36 description: "Nil/A", 37 args: args{ 38 b: &configv1.ClusterOperatorStatusCondition{}, 39 }, 40 expect: false, 41 }, 42 { 43 description: "Nil/B", 44 args: args{ 45 a: &configv1.ClusterOperatorStatusCondition{}, 46 }, 47 expect: false, 48 }, 49 { 50 description: "Same", 51 args: args{ 52 a: &configv1.ClusterOperatorStatusCondition{}, 53 b: &configv1.ClusterOperatorStatusCondition{}, 54 }, 55 expect: true, 56 }, 57 { 58 description: "Different/LastTransitionTime", 59 args: args{ 60 a: &configv1.ClusterOperatorStatusCondition{ 61 LastTransitionTime: metav1.Now(), 62 }, 63 b: &configv1.ClusterOperatorStatusCondition{}, 64 }, 65 expect: true, 66 }, 67 { 68 description: "Different/Status", 69 args: args{ 70 a: &configv1.ClusterOperatorStatusCondition{ 71 Status: configv1.ConditionTrue, 72 }, 73 b: &configv1.ClusterOperatorStatusCondition{}, 74 }, 75 expect: false, 76 }, 77 } { 78 t.Run(tt.description, func(t *testing.T) { 79 require.Equal(t, tt.expect, conditionsEqual(tt.args.a, tt.args.b)) 80 }) 81 } 82 } 83 84 func TestVersionsMatch(t *testing.T) { 85 type in struct { 86 a, b []configv1.OperandVersion 87 } 88 for _, tt := range []struct { 89 description string 90 in in 91 expect bool 92 }{ 93 { 94 description: "Different/Nil", 95 in: in{ 96 a: []configv1.OperandVersion{ 97 {Name: "weyland", Version: "1.0.0"}, 98 }, 99 b: nil, 100 }, 101 expect: false, 102 }, 103 { 104 description: "Different/Names", 105 in: in{ 106 a: []configv1.OperandVersion{ 107 {Name: "weyland", Version: "1.0.0"}, 108 }, 109 b: []configv1.OperandVersion{ 110 {Name: "yutani", Version: "1.0.0"}, 111 }, 112 }, 113 expect: false, 114 }, 115 { 116 description: "Different/Versions", 117 in: in{ 118 a: []configv1.OperandVersion{ 119 {Name: "weyland", Version: "1.0.0"}, 120 }, 121 b: []configv1.OperandVersion{ 122 {Name: "weyland", Version: "2.0.0"}, 123 }, 124 }, 125 expect: false, 126 }, 127 { 128 description: "Different/Lengths", 129 in: in{ 130 a: []configv1.OperandVersion{ 131 {Name: "weyland", Version: "1.0.0"}, 132 }, 133 b: []configv1.OperandVersion{ 134 {Name: "weyland", Version: "1.0.0"}, 135 {Name: "yutani", Version: "1.0.0"}, 136 }, 137 }, 138 expect: false, 139 }, 140 { 141 description: "Different/Elements", 142 in: in{ 143 a: []configv1.OperandVersion{ 144 {Name: "weyland", Version: "1.0.0"}, 145 {Name: "weyland", Version: "1.0.0"}, 146 {Name: "weyland", Version: "1.0.0"}, 147 {Name: "yutani", Version: "1.0.0"}, 148 }, 149 b: []configv1.OperandVersion{ 150 {Name: "weyland", Version: "1.0.0"}, 151 {Name: "weyland", Version: "1.0.0"}, 152 {Name: "yutani", Version: "1.0.0"}, 153 {Name: "yutani", Version: "1.0.0"}, 154 }, 155 }, 156 expect: false, 157 }, 158 { 159 description: "Same/Nil", 160 in: in{ 161 a: nil, 162 b: nil, 163 }, 164 expect: true, 165 }, 166 { 167 description: "Same/Empty", 168 in: in{ 169 a: []configv1.OperandVersion{}, 170 b: []configv1.OperandVersion{}, 171 }, 172 expect: true, 173 }, 174 { 175 description: "Same/Empty/Nil", 176 in: in{ 177 a: []configv1.OperandVersion{}, 178 b: nil, 179 }, 180 expect: true, 181 }, 182 { 183 description: "Same", 184 in: in{ 185 a: []configv1.OperandVersion{ 186 {Name: "weyland", Version: "1.0.0"}, 187 {Name: "yutani", Version: "1.0.0"}, 188 }, 189 b: []configv1.OperandVersion{ 190 {Name: "weyland", Version: "1.0.0"}, 191 {Name: "yutani", Version: "1.0.0"}, 192 }, 193 }, 194 expect: true, 195 }, 196 { 197 description: "Same/Unordered", 198 in: in{ 199 a: []configv1.OperandVersion{ 200 {Name: "weyland", Version: "1.0.0"}, 201 {Name: "yutani", Version: "1.0.0"}, 202 }, 203 b: []configv1.OperandVersion{ 204 {Name: "yutani", Version: "1.0.0"}, 205 {Name: "weyland", Version: "1.0.0"}, 206 }, 207 }, 208 expect: true, 209 }, 210 } { 211 t.Run(tt.description, func(t *testing.T) { 212 require.Equal(t, tt.expect, versionsMatch(tt.in.a, tt.in.b)) 213 }) 214 } 215 } 216 217 func TestIncompatibleOperators(t *testing.T) { 218 type expect struct { 219 err bool 220 incompatible skews 221 } 222 for _, tt := range []struct { 223 description string 224 version string 225 in skews 226 expect expect 227 }{ 228 { 229 description: "Compatible", 230 version: "1.0.0", 231 in: skews{ 232 { 233 name: "almond", 234 namespace: "default", 235 maxOpenShiftVersion: "1.1.0", 236 }, 237 { 238 name: "beech", 239 namespace: "default", 240 maxOpenShiftVersion: "1.1.0+build", 241 }, 242 { 243 name: "chestnut", 244 namespace: "default", 245 maxOpenShiftVersion: "2.0.0", 246 }, 247 }, 248 expect: expect{ 249 err: false, 250 incompatible: nil, 251 }, 252 }, 253 { 254 description: "Incompatible", 255 version: "1.0.0", 256 in: skews{ 257 { 258 name: "almond", 259 namespace: "default", 260 maxOpenShiftVersion: "1.0.0", 261 }, 262 { 263 name: "beech", 264 namespace: "default", 265 maxOpenShiftVersion: "1.0.0+build", 266 }, 267 { 268 name: "chestnut", 269 namespace: "default", 270 maxOpenShiftVersion: "1.1.0-pre", 271 }, 272 { 273 name: "drupe", 274 namespace: "default", 275 maxOpenShiftVersion: "1.1.0-pre+build", 276 }, 277 { 278 name: "european-hazelnut", 279 namespace: "default", 280 maxOpenShiftVersion: "0.1.0", 281 }, 282 }, 283 expect: expect{ 284 err: false, 285 incompatible: skews{ 286 { 287 name: "almond", 288 namespace: "default", 289 maxOpenShiftVersion: "1.0", 290 }, 291 { 292 name: "beech", 293 namespace: "default", 294 maxOpenShiftVersion: "1.0", 295 }, 296 { 297 name: "chestnut", 298 namespace: "default", 299 err: fmt.Errorf("property olm.maxOpenShiftVersion must specify only <major>.<minor> version, got invalid value 1.1.0-pre"), 300 }, 301 { 302 name: "drupe", 303 namespace: "default", 304 err: fmt.Errorf("property olm.maxOpenShiftVersion must specify only <major>.<minor> version, got invalid value 1.1.0-pre+build"), 305 }, 306 { 307 name: "european-hazelnut", 308 namespace: "default", 309 maxOpenShiftVersion: "0.1", 310 }, 311 }, 312 }, 313 }, 314 { 315 description: "Mixed", 316 version: "1.0.0", 317 in: skews{ 318 { 319 name: "almond", 320 namespace: "default", 321 maxOpenShiftVersion: "1.1.0", 322 }, 323 { 324 name: "beech", 325 namespace: "default", 326 maxOpenShiftVersion: "1.0.0", 327 }, 328 { 329 name: "chestnut", 330 namespace: "default", 331 maxOpenShiftVersion: "1.0", 332 }, 333 }, 334 expect: expect{ 335 err: false, 336 incompatible: skews{ 337 { 338 name: "beech", 339 namespace: "default", 340 maxOpenShiftVersion: "1.0", 341 }, 342 { 343 name: "chestnut", 344 namespace: "default", 345 maxOpenShiftVersion: "1.0", 346 }, 347 }, 348 }, 349 }, 350 { 351 description: "Mixed/BadVersion", 352 version: "1.0.0", 353 in: skews{ 354 { 355 name: "almond", 356 namespace: "default", 357 maxOpenShiftVersion: "1.1.0", 358 }, 359 { 360 name: "beech", 361 namespace: "default", 362 maxOpenShiftVersion: "1.0.0", 363 }, 364 { 365 name: "chestnut", 366 namespace: "default", 367 maxOpenShiftVersion: "bad_version", 368 }, 369 }, 370 expect: expect{ 371 err: false, 372 incompatible: skews{ 373 { 374 name: "beech", 375 namespace: "default", 376 maxOpenShiftVersion: "1.0", 377 }, 378 { 379 name: "chestnut", 380 namespace: "default", 381 err: fmt.Errorf(`failed to parse "bad_version" as semver: %w`, func() error { 382 _, err := semver.ParseTolerant("bad_version") 383 return err 384 }()), 385 }, 386 }, 387 }, 388 }, 389 { 390 description: "EmptyVersion", 391 version: "", // This should result in an transient error 392 in: skews{ 393 { 394 name: "almond", 395 namespace: "default", 396 maxOpenShiftVersion: "1.1.0", 397 }, 398 { 399 name: "beech", 400 namespace: "default", 401 maxOpenShiftVersion: "1.0.0", 402 }, 403 }, 404 expect: expect{ 405 err: true, 406 incompatible: nil, 407 }, 408 }, 409 { 410 description: "ClusterZ", 411 version: "1.0.1", // Next Y-stream is 1.1.0, NOT 1.1.1 412 in: skews{ 413 { 414 name: "beech", 415 namespace: "default", 416 maxOpenShiftVersion: "1.1", 417 }, 418 }, 419 expect: expect{ 420 err: false, 421 incompatible: nil, 422 }, 423 }, 424 { 425 description: "ClusterPre", 426 version: "1.1.0-pre", // Next Y-stream is 1.2.0 427 in: skews{ 428 { 429 name: "almond", 430 namespace: "default", 431 maxOpenShiftVersion: "1.1.0", 432 }, 433 }, 434 expect: expect{ 435 err: false, 436 incompatible: skews{ 437 { 438 name: "almond", 439 namespace: "default", 440 maxOpenShiftVersion: "1.1", 441 }, 442 }, 443 }, 444 }, 445 } { 446 t.Run(tt.description, func(t *testing.T) { 447 objs := []client.Object{} 448 449 resetCurrentReleaseTo(tt.version) 450 451 for _, s := range tt.in { 452 csv := &operatorsv1alpha1.ClusterServiceVersion{} 453 csv.SetName(s.name) 454 csv.SetNamespace(s.namespace) 455 456 maxProperty := &api.Property{ 457 Type: MaxOpenShiftVersionProperty, 458 Value: `"` + s.maxOpenShiftVersion + `"`, // Wrap in quotes so we don't break property marshaling 459 } 460 value, err := projection.PropertiesAnnotationFromPropertyList([]*api.Property{maxProperty}) 461 require.NoError(t, err) 462 463 csv.SetAnnotations(map[string]string{ 464 projection.PropertiesAnnotationKey: value, 465 }) 466 467 objs = append(objs, csv) 468 } 469 470 scheme := runtime.NewScheme() 471 require.NoError(t, AddToScheme(scheme)) 472 473 fcli := fake.NewClientBuilder().WithScheme(scheme).WithObjects(objs...).Build() 474 incompatible, err := incompatibleOperators(context.Background(), fcli) 475 if tt.expect.err { 476 require.Error(t, err) 477 } else { 478 require.NoError(t, err) 479 } 480 481 require.ElementsMatch(t, tt.expect.incompatible, incompatible) 482 }) 483 } 484 } 485 486 func TestMaxOpenShiftVersion(t *testing.T) { 487 mustParse := func(s string) *semver.Version { 488 version, err := semver.ParseTolerant(s) 489 if err != nil { 490 panic(fmt.Sprintf("bad version given for test case: %s", err)) 491 } 492 return &version 493 } 494 495 type expect struct { 496 err bool 497 max *semver.Version 498 } 499 for _, tt := range []struct { 500 description string 501 in []string 502 expect expect 503 }{ 504 { 505 description: "None", 506 expect: expect{ 507 err: false, 508 max: nil, 509 }, 510 }, 511 { 512 description: "Nothing", 513 in: []string{`""`}, 514 expect: expect{ 515 err: true, 516 max: nil, 517 }, 518 }, 519 { 520 description: "Garbage", 521 in: []string{`"bad_version"`}, 522 expect: expect{ 523 err: true, 524 max: nil, 525 }, 526 }, 527 { 528 description: "Single", 529 in: []string{`"1.0.0"`}, 530 expect: expect{ 531 err: false, 532 max: mustParse("1.0.0"), 533 }, 534 }, 535 { 536 description: "Multiple", 537 in: []string{ 538 `"1.0.0"`, 539 `"2.0.0"`, 540 }, 541 expect: expect{ 542 err: true, 543 max: nil, 544 }, 545 }, 546 { 547 // Ensure unquoted short strings are accepted; e.g. X.Y 548 description: "Unquoted/Short", 549 in: []string{"4.8"}, 550 expect: expect{ 551 err: false, 552 max: mustParse("4.8"), 553 }, 554 }, 555 } { 556 t.Run(tt.description, func(t *testing.T) { 557 var properties []*api.Property 558 for _, max := range tt.in { 559 properties = append(properties, &api.Property{ 560 Type: MaxOpenShiftVersionProperty, 561 Value: max, 562 }) 563 } 564 565 value, err := projection.PropertiesAnnotationFromPropertyList(properties) 566 require.NoError(t, err) 567 568 csv := &operatorsv1alpha1.ClusterServiceVersion{} 569 csv.SetAnnotations(map[string]string{ 570 projection.PropertiesAnnotationKey: value, 571 }) 572 573 max, err := maxOpenShiftVersion(csv) 574 if tt.expect.err { 575 require.Error(t, err) 576 } else { 577 require.NoError(t, err) 578 } 579 580 require.Equal(t, tt.expect.max, max) 581 }) 582 } 583 } 584 585 func TestNotCopiedSelector(t *testing.T) { 586 for _, tc := range []struct { 587 Labels labels.Set 588 Matches bool 589 }{ 590 { 591 Labels: labels.Set{operatorsv1alpha1.CopiedLabelKey: ""}, 592 Matches: false, 593 }, 594 { 595 Labels: labels.Set{}, 596 Matches: true, 597 }, 598 } { 599 t.Run(tc.Labels.String(), func(t *testing.T) { 600 selector, err := notCopiedSelector() 601 require.NoError(t, err) 602 require.Equal(t, tc.Matches, selector.Matches(tc.Labels)) 603 }) 604 } 605 } 606 607 func TestOCPVersionNextY(t *testing.T) { 608 for _, tc := range []struct { 609 description string 610 inVersion semver.Version 611 expectedVersion semver.Version 612 }{ 613 { 614 description: "Version: 4.16.0. Expected output: 4.17", 615 inVersion: semver.MustParse("4.16.0"), 616 expectedVersion: semver.MustParse("4.17.0"), 617 }, 618 { 619 description: "Version: 4.16.0-rc1. Expected output: 4.17", 620 inVersion: semver.MustParse("4.16.0-rc1"), 621 expectedVersion: semver.MustParse("4.17.0"), 622 }, 623 } { 624 t.Run(tc.description, func(t *testing.T) { 625 outVersion := nextY(tc.inVersion) 626 require.Equal(t, outVersion, tc.expectedVersion) 627 }) 628 } 629 }