github.com/operator-framework/operator-lifecycle-manager@v0.30.0/pkg/controller/install/deployment_test.go (about) 1 package install 2 3 import ( 4 "fmt" 5 "testing" 6 7 hashutil "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/kubernetes/pkg/util/hash" 8 "github.com/stretchr/testify/assert" 9 "github.com/stretchr/testify/require" 10 appsv1 "k8s.io/api/apps/v1" 11 corev1 "k8s.io/api/core/v1" 12 rbacv1 "k8s.io/api/rbac/v1" 13 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 k8slabels "k8s.io/apimachinery/pkg/labels" 15 16 "github.com/operator-framework/api/pkg/operators/v1alpha1" 17 clientfakes "github.com/operator-framework/operator-lifecycle-manager/pkg/api/wrappers/wrappersfakes" 18 "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/kubernetes/pkg/util/labels" 19 "github.com/operator-framework/operator-lifecycle-manager/pkg/lib/ownerutil" 20 ) 21 22 func testDeployment(name, namespace string, mockOwner ownerutil.Owner) appsv1.Deployment { 23 testDeploymentLabels := map[string]string{"olm.owner": mockOwner.GetName(), "olm.owner.namespace": mockOwner.GetNamespace(), "olm.owner.kind": "ClusterServiceVersion"} 24 25 deployment := appsv1.Deployment{ 26 ObjectMeta: metav1.ObjectMeta{ 27 Namespace: namespace, 28 Name: name, 29 OwnerReferences: []metav1.OwnerReference{ 30 { 31 APIVersion: v1alpha1.SchemeGroupVersion.String(), 32 Kind: v1alpha1.ClusterServiceVersionKind, 33 Name: mockOwner.GetName(), 34 UID: mockOwner.GetUID(), 35 Controller: &ownerutil.NotController, 36 BlockOwnerDeletion: &ownerutil.DontBlockOwnerDeletion, 37 }, 38 }, 39 Labels: testDeploymentLabels, 40 }, 41 } 42 return deployment 43 } 44 45 func testServiceAccount(name string, mockOwner ownerutil.Owner) *corev1.ServiceAccount { 46 serviceAccount := &corev1.ServiceAccount{} 47 serviceAccount.SetName(name) 48 serviceAccount.SetOwnerReferences([]metav1.OwnerReference{ 49 { 50 APIVersion: v1alpha1.SchemeGroupVersion.String(), 51 Kind: v1alpha1.ClusterServiceVersionKind, 52 Name: mockOwner.GetName(), 53 UID: mockOwner.GetUID(), 54 Controller: &ownerutil.NotController, 55 BlockOwnerDeletion: &ownerutil.DontBlockOwnerDeletion, 56 }, 57 }) 58 return serviceAccount 59 } 60 61 func strategy(n int, namespace string, mockOwner ownerutil.Owner) *v1alpha1.StrategyDetailsDeployment { 62 var deploymentSpecs = []v1alpha1.StrategyDeploymentSpec{} 63 var permissions = []v1alpha1.StrategyDeploymentPermissions{} 64 for i := 1; i <= n; i++ { 65 dep := testDeployment(fmt.Sprintf("olm-dep-%d", i), namespace, mockOwner) 66 spec := v1alpha1.StrategyDeploymentSpec{Name: dep.GetName(), Spec: dep.Spec} 67 deploymentSpecs = append(deploymentSpecs, spec) 68 serviceAccount := testServiceAccount(fmt.Sprintf("olm-sa-%d", i), mockOwner) 69 permissions = append(permissions, v1alpha1.StrategyDeploymentPermissions{ 70 ServiceAccountName: serviceAccount.Name, 71 Rules: []rbacv1.PolicyRule{ 72 { 73 Verbs: []string{"list", "delete"}, 74 APIGroups: []string{""}, 75 Resources: []string{"pods"}, 76 }, 77 }, 78 }) 79 } 80 return &v1alpha1.StrategyDetailsDeployment{ 81 DeploymentSpecs: deploymentSpecs, 82 Permissions: permissions, 83 } 84 } 85 86 func TestInstallStrategyDeploymentInstallDeployments(t *testing.T) { 87 var ( 88 mockOwner = v1alpha1.ClusterServiceVersion{ 89 TypeMeta: metav1.TypeMeta{ 90 Kind: v1alpha1.ClusterServiceVersionKind, 91 APIVersion: v1alpha1.ClusterServiceVersionAPIVersion, 92 }, 93 ObjectMeta: metav1.ObjectMeta{ 94 Name: "clusterserviceversion-owner", 95 Namespace: "olm-test-deployment", 96 }, 97 } 98 mockOwnerRefs = []metav1.OwnerReference{{ 99 APIVersion: v1alpha1.ClusterServiceVersionAPIVersion, 100 Kind: v1alpha1.ClusterServiceVersionKind, 101 Name: mockOwner.GetName(), 102 UID: mockOwner.UID, 103 Controller: &ownerutil.NotController, 104 BlockOwnerDeletion: &ownerutil.DontBlockOwnerDeletion, 105 }} 106 expectedRevisionHistoryLimit = int32(1) 107 defaultRevisionHistoryLimit = int32(10) 108 ) 109 110 type inputs struct { 111 strategyDeploymentSpecs []v1alpha1.StrategyDeploymentSpec 112 } 113 type setup struct { 114 existingDeployments []*appsv1.Deployment 115 } 116 type createOrUpdateMock struct { 117 expectedDeployment appsv1.Deployment 118 returnError error 119 } 120 tests := []struct { 121 description string 122 inputs inputs 123 setup setup 124 createOrUpdateMocks []createOrUpdateMock 125 output error 126 }{ 127 { 128 description: "updates/creates correctly", 129 inputs: inputs{ 130 strategyDeploymentSpecs: []v1alpha1.StrategyDeploymentSpec{ 131 { 132 Name: "test-deployment-1", 133 Spec: appsv1.DeploymentSpec{ 134 RevisionHistoryLimit: &defaultRevisionHistoryLimit, 135 }, 136 }, 137 { 138 Name: "test-deployment-2", 139 Spec: appsv1.DeploymentSpec{ 140 RevisionHistoryLimit: nil, 141 }, 142 }, 143 { 144 Name: "test-deployment-3", 145 Spec: appsv1.DeploymentSpec{}, 146 }, 147 { 148 Name: "test-deployment-4", 149 Spec: appsv1.DeploymentSpec{}, 150 Label: k8slabels.Set{"custom-label": "custom-label-value"}, 151 }, 152 }, 153 }, 154 setup: setup{ 155 existingDeployments: []*appsv1.Deployment{ 156 { 157 ObjectMeta: metav1.ObjectMeta{ 158 Name: "test-deployment-1", 159 }, 160 }, 161 { 162 ObjectMeta: metav1.ObjectMeta{ 163 Name: "test-deployment-3", 164 }, 165 Spec: appsv1.DeploymentSpec{ 166 Paused: false, // arbitrary spec difference 167 }, 168 }, 169 }, 170 }, 171 createOrUpdateMocks: []createOrUpdateMock{ 172 { 173 expectedDeployment: appsv1.Deployment{ 174 ObjectMeta: metav1.ObjectMeta{ 175 Name: "test-deployment-1", 176 Namespace: mockOwner.GetNamespace(), 177 OwnerReferences: mockOwnerRefs, 178 Labels: map[string]string{ 179 "olm.owner": mockOwner.GetName(), 180 "olm.owner.namespace": mockOwner.GetNamespace(), 181 }, 182 }, 183 Spec: appsv1.DeploymentSpec{ 184 RevisionHistoryLimit: &expectedRevisionHistoryLimit, 185 Template: corev1.PodTemplateSpec{ 186 ObjectMeta: metav1.ObjectMeta{ 187 Annotations: map[string]string{}, 188 }, 189 }, 190 }, 191 }, 192 returnError: nil, 193 }, 194 { 195 expectedDeployment: appsv1.Deployment{ 196 ObjectMeta: metav1.ObjectMeta{ 197 Name: "test-deployment-2", 198 Namespace: mockOwner.GetNamespace(), 199 OwnerReferences: mockOwnerRefs, 200 Labels: map[string]string{ 201 "olm.owner": mockOwner.GetName(), 202 "olm.owner.namespace": mockOwner.GetNamespace(), 203 }, 204 }, 205 Spec: appsv1.DeploymentSpec{ 206 RevisionHistoryLimit: &expectedRevisionHistoryLimit, 207 Template: corev1.PodTemplateSpec{ 208 ObjectMeta: metav1.ObjectMeta{ 209 Annotations: map[string]string{}, 210 }, 211 }, 212 }, 213 }, 214 returnError: nil, 215 }, 216 { 217 expectedDeployment: appsv1.Deployment{ 218 ObjectMeta: metav1.ObjectMeta{ 219 Name: "test-deployment-3", 220 Namespace: mockOwner.GetNamespace(), 221 OwnerReferences: mockOwnerRefs, 222 Labels: map[string]string{ 223 "olm.owner": mockOwner.GetName(), 224 "olm.owner.namespace": mockOwner.GetNamespace(), 225 }, 226 }, 227 Spec: appsv1.DeploymentSpec{ 228 RevisionHistoryLimit: &expectedRevisionHistoryLimit, 229 Template: corev1.PodTemplateSpec{ 230 ObjectMeta: metav1.ObjectMeta{ 231 Annotations: map[string]string{}, 232 }, 233 }, 234 }, 235 }, 236 returnError: nil, 237 }, 238 { 239 expectedDeployment: appsv1.Deployment{ 240 ObjectMeta: metav1.ObjectMeta{ 241 Name: "test-deployment-4", 242 Namespace: mockOwner.GetNamespace(), 243 OwnerReferences: mockOwnerRefs, 244 Labels: map[string]string{ 245 "olm.owner": mockOwner.GetName(), 246 "olm.owner.namespace": mockOwner.GetNamespace(), 247 "custom-label": "custom-label-value", 248 }, 249 }, 250 Spec: appsv1.DeploymentSpec{ 251 RevisionHistoryLimit: &expectedRevisionHistoryLimit, 252 Template: corev1.PodTemplateSpec{ 253 ObjectMeta: metav1.ObjectMeta{ 254 Annotations: map[string]string{}, 255 }, 256 }, 257 }, 258 }, 259 returnError: nil, 260 }, 261 }, 262 output: nil, 263 }, 264 } 265 266 for _, tt := range tests { 267 t.Run(tt.description, func(t *testing.T) { 268 fakeClient := new(clientfakes.FakeInstallStrategyDeploymentInterface) 269 270 for i, m := range tt.createOrUpdateMocks { 271 fakeClient.CreateDeploymentReturns(nil, m.returnError) 272 defer func(i int, expectedDeployment appsv1.Deployment) { 273 dep := fakeClient.CreateOrUpdateDeploymentArgsForCall(i) 274 expectedDeployment.Spec.Template.Annotations = map[string]string{} 275 require.Equal(t, expectedDeployment.OwnerReferences, dep.OwnerReferences) 276 for labelKey, labelValue := range expectedDeployment.Labels { 277 require.Contains(t, dep.GetLabels(), labelKey) 278 require.Equal(t, dep.Labels[labelKey], labelValue) 279 } 280 require.Equal(t, expectedDeployment.Spec.RevisionHistoryLimit, dep.Spec.RevisionHistoryLimit) 281 }(i, m.expectedDeployment) 282 } 283 284 installer := &StrategyDeploymentInstaller{ 285 strategyClient: fakeClient, 286 owner: &mockOwner, 287 } 288 result := installer.installDeployments(tt.inputs.strategyDeploymentSpecs) 289 assert.Equal(t, tt.output, result) 290 }) 291 } 292 } 293 294 type BadStrategy struct{} 295 296 func (b *BadStrategy) GetStrategyName() string { 297 return "bad" 298 } 299 300 func TestNewStrategyDeploymentInstaller(t *testing.T) { 301 mockOwner := v1alpha1.ClusterServiceVersion{ 302 TypeMeta: metav1.TypeMeta{ 303 Kind: v1alpha1.ClusterServiceVersionKind, 304 APIVersion: v1alpha1.ClusterServiceVersionAPIVersion, 305 }, 306 ObjectMeta: metav1.ObjectMeta{ 307 Name: "clusterserviceversion-owner", 308 Namespace: "ns", 309 }, 310 } 311 fakeClient := new(clientfakes.FakeInstallStrategyDeploymentInterface) 312 strategy := NewStrategyDeploymentInstaller(fakeClient, map[string]string{"test": "annotation"}, &mockOwner, nil, nil, nil, nil) 313 require.Implements(t, (*StrategyInstaller)(nil), strategy) 314 require.Error(t, strategy.Install(&BadStrategy{})) 315 installed, err := strategy.CheckInstalled(&BadStrategy{}) 316 require.False(t, installed) 317 require.Error(t, err) 318 } 319 320 func TestInstallStrategyDeploymentCheckInstallErrors(t *testing.T) { 321 namespace := "olm-test-deployment" 322 323 mockOwner := v1alpha1.ClusterServiceVersion{ 324 TypeMeta: metav1.TypeMeta{ 325 Kind: v1alpha1.ClusterServiceVersionKind, 326 APIVersion: v1alpha1.ClusterServiceVersionAPIVersion, 327 }, 328 ObjectMeta: metav1.ObjectMeta{ 329 Name: "clusterserviceversion-owner", 330 Namespace: namespace, 331 }, 332 } 333 334 mockOwnerLabel := ownerutil.CSVOwnerSelector(&mockOwner) 335 336 tests := []struct { 337 createDeploymentErr error 338 description string 339 }{ 340 { 341 createDeploymentErr: fmt.Errorf("error creating deployment"), 342 description: "ErrorCreatingDeployment", 343 }, 344 } 345 346 revisionHistoryLimit := int32(1) 347 for _, tt := range tests { 348 t.Run(tt.description, func(t *testing.T) { 349 fakeClient := new(clientfakes.FakeInstallStrategyDeploymentInterface) 350 strategy := strategy(1, namespace, &mockOwner) 351 installer := NewStrategyDeploymentInstaller(fakeClient, map[string]string{"test": "annotation"}, &mockOwner, nil, nil, nil, nil) 352 353 dep := testDeployment("olm-dep-1", namespace, &mockOwner) 354 dep.Spec.Template.SetAnnotations(map[string]string{"test": "annotation"}) 355 dep.Spec.RevisionHistoryLimit = &revisionHistoryLimit 356 hash, err := hashutil.DeepHashObject(&dep.Spec) 357 if err != nil { 358 t.Fatal(err) 359 } 360 dep.SetLabels(labels.CloneAndAddLabel(dep.ObjectMeta.GetLabels(), DeploymentSpecHashLabelKey, hash)) 361 dep.Labels[OLMManagedLabelKey] = OLMManagedLabelValue 362 dep.Status.Conditions = append(dep.Status.Conditions, appsv1.DeploymentCondition{ 363 Type: appsv1.DeploymentAvailable, 364 Status: corev1.ConditionTrue, 365 }) 366 fakeClient.FindAnyDeploymentsMatchingLabelsReturns( 367 []*appsv1.Deployment{ 368 &dep, 369 }, nil, 370 ) 371 defer func() { 372 require.Equal(t, mockOwnerLabel, fakeClient.FindAnyDeploymentsMatchingLabelsArgsForCall(0)) 373 }() 374 375 installed, err := installer.CheckInstalled(strategy) 376 require.NoError(t, err) 377 require.True(t, installed) 378 379 deployment := testDeployment("olm-dep-1", namespace, &mockOwner) 380 deployment.Spec.Template.SetAnnotations(map[string]string{"test": "annotation"}) 381 deployment.Spec.RevisionHistoryLimit = &revisionHistoryLimit 382 hash, err = hashutil.DeepHashObject(&deployment.Spec) 383 if err != nil { 384 t.Fatal(err) 385 } 386 deployment.SetLabels(labels.CloneAndAddLabel(dep.ObjectMeta.GetLabels(), DeploymentSpecHashLabelKey, hash)) 387 fakeClient.CreateOrUpdateDeploymentReturns(&deployment, tt.createDeploymentErr) 388 defer func() { 389 require.Equal(t, &deployment, fakeClient.CreateOrUpdateDeploymentArgsForCall(0)) 390 }() 391 392 if tt.createDeploymentErr != nil { 393 err := installer.Install(strategy) 394 require.Error(t, err) 395 } 396 }) 397 } 398 } 399 400 func TestInstallStrategyDeploymentCleanupDeployments(t *testing.T) { 401 var ( 402 mockOwner = v1alpha1.ClusterServiceVersion{ 403 TypeMeta: metav1.TypeMeta{ 404 Kind: v1alpha1.ClusterServiceVersionKind, 405 APIVersion: v1alpha1.ClusterServiceVersionAPIVersion, 406 }, 407 ObjectMeta: metav1.ObjectMeta{ 408 Name: "clusterserviceversion-owner", 409 Namespace: "olm-test-deployment", 410 }, 411 } 412 mockOwnerRefs = []metav1.OwnerReference{{ 413 APIVersion: v1alpha1.ClusterServiceVersionAPIVersion, 414 Kind: v1alpha1.ClusterServiceVersionKind, 415 Name: mockOwner.GetName(), 416 UID: mockOwner.UID, 417 Controller: &ownerutil.NotController, 418 BlockOwnerDeletion: &ownerutil.DontBlockOwnerDeletion, 419 }} 420 ) 421 422 type inputs struct { 423 strategyDeploymentSpecs []v1alpha1.StrategyDeploymentSpec 424 } 425 type setup struct { 426 existingDeployments []*appsv1.Deployment 427 returnError error 428 } 429 type cleanupMock struct { 430 deletedDeploymentName string 431 returnError error 432 } 433 tests := []struct { 434 description string 435 inputs inputs 436 setup setup 437 cleanupMock cleanupMock 438 output error 439 }{ 440 { 441 description: "cleanup successfully", 442 inputs: inputs{ 443 strategyDeploymentSpecs: []v1alpha1.StrategyDeploymentSpec{ 444 { 445 Name: "test-deployment-1", 446 Spec: appsv1.DeploymentSpec{}, 447 }, 448 }, 449 }, 450 setup: setup{ 451 existingDeployments: []*appsv1.Deployment{ 452 { 453 ObjectMeta: metav1.ObjectMeta{ 454 Name: "test-deployment-2", 455 Namespace: mockOwner.GetNamespace(), 456 OwnerReferences: mockOwnerRefs, 457 Labels: map[string]string{ 458 "olm.owner": mockOwner.GetName(), 459 "olm.owner.namespace": mockOwner.GetNamespace(), 460 }, 461 }, 462 }, 463 }, 464 returnError: nil, 465 }, 466 cleanupMock: cleanupMock{ 467 deletedDeploymentName: "test-deployment-2", 468 returnError: nil, 469 }, 470 output: nil, 471 }, 472 { 473 description: "cleanup unsuccessfully as no orphaned deployments found", 474 inputs: inputs{ 475 strategyDeploymentSpecs: []v1alpha1.StrategyDeploymentSpec{ 476 { 477 Name: "test-deployment-1", 478 Spec: appsv1.DeploymentSpec{}, 479 }, 480 }, 481 }, 482 setup: setup{ 483 existingDeployments: []*appsv1.Deployment{}, 484 returnError: fmt.Errorf("error getting deployments"), 485 }, 486 cleanupMock: cleanupMock{ 487 deletedDeploymentName: "", 488 returnError: nil, 489 }, 490 output: fmt.Errorf("error getting deployments"), 491 }, 492 { 493 description: "cleanup unsuccessfully as unable to look up orphaned deployments", 494 inputs: inputs{ 495 strategyDeploymentSpecs: []v1alpha1.StrategyDeploymentSpec{ 496 { 497 Name: "test-deployment-1", 498 Spec: appsv1.DeploymentSpec{}, 499 }, 500 }, 501 }, 502 setup: setup{ 503 existingDeployments: []*appsv1.Deployment{}, 504 returnError: fmt.Errorf("error unable to look up orphaned deployments"), 505 }, 506 cleanupMock: cleanupMock{ 507 deletedDeploymentName: "", 508 returnError: nil, 509 }, 510 output: fmt.Errorf("error unable to look up orphaned deployments"), 511 }, 512 { 513 description: "cleanup unsuccessfully as unable to delete deployments", 514 inputs: inputs{ 515 strategyDeploymentSpecs: []v1alpha1.StrategyDeploymentSpec{ 516 { 517 Name: "test-deployment-1", 518 Spec: appsv1.DeploymentSpec{}, 519 }, 520 }, 521 }, 522 setup: setup{ 523 existingDeployments: []*appsv1.Deployment{ 524 { 525 ObjectMeta: metav1.ObjectMeta{ 526 Name: "test-deployment-2", 527 Namespace: mockOwner.GetNamespace(), 528 OwnerReferences: mockOwnerRefs, 529 Labels: map[string]string{ 530 "olm.owner": mockOwner.GetName(), 531 "olm.owner.namespace": mockOwner.GetNamespace(), 532 }, 533 }, 534 }, 535 }, 536 returnError: nil, 537 }, 538 cleanupMock: cleanupMock{ 539 deletedDeploymentName: "", 540 returnError: fmt.Errorf("error unable to delete deployments"), 541 }, 542 output: fmt.Errorf("error unable to delete deployments"), 543 }, 544 } 545 546 for _, tt := range tests { 547 t.Run(tt.description, func(t *testing.T) { 548 fakeClient := new(clientfakes.FakeInstallStrategyDeploymentInterface) 549 installer := &StrategyDeploymentInstaller{ 550 strategyClient: fakeClient, 551 owner: &mockOwner, 552 } 553 554 fakeClient.FindAnyDeploymentsMatchingLabelsReturns( 555 tt.setup.existingDeployments, tt.setup.returnError, 556 ) 557 558 fakeClient.DeleteDeploymentReturns(tt.cleanupMock.returnError) 559 560 if tt.setup.returnError == nil && tt.cleanupMock.returnError == nil { 561 defer func() { 562 deletedDep := fakeClient.DeleteDeploymentArgsForCall(0) 563 require.Equal(t, tt.cleanupMock.deletedDeploymentName, deletedDep) 564 }() 565 } 566 567 result := installer.cleanupOrphanedDeployments(tt.inputs.strategyDeploymentSpecs) 568 assert.Equal(t, tt.output, result) 569 }) 570 } 571 }