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  }