github.com/verrazzano/verrazzano@v1.7.0/application-operator/controllers/loggingtrait/loggingtrait_controller_test.go (about) 1 // Copyright (c) 2021, 2023, Oracle and/or its affiliates. 2 // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. 3 4 package loggingtrait 5 6 import ( 7 "context" 8 "encoding/json" 9 "fmt" 10 "testing" 11 "time" 12 13 vzapi "github.com/verrazzano/verrazzano/application-operator/apis/oam/v1alpha1" 14 "github.com/verrazzano/verrazzano/application-operator/mocks" 15 vzconst "github.com/verrazzano/verrazzano/pkg/constants" 16 "github.com/verrazzano/verrazzano/pkg/log/vzlog" 17 18 oamrt "github.com/crossplane/crossplane-runtime/apis/common/v1" 19 oamcore "github.com/crossplane/oam-kubernetes-runtime/apis/core/v1alpha2" 20 "github.com/go-logr/logr" 21 "github.com/golang/mock/gomock" 22 asserts "github.com/stretchr/testify/assert" 23 "go.uber.org/zap" 24 k8sapps "k8s.io/api/apps/v1" 25 corev1 "k8s.io/api/core/v1" 26 "k8s.io/apimachinery/pkg/api/errors" 27 k8smeta "k8s.io/apimachinery/pkg/apis/meta/v1" 28 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 29 "k8s.io/apimachinery/pkg/runtime" 30 "k8s.io/apimachinery/pkg/types" 31 ctrl "sigs.k8s.io/controller-runtime" 32 "sigs.k8s.io/controller-runtime/pkg/client" 33 // +kubebuilder:scaffold:imports 34 ) 35 36 var ( 37 namespaceName = "test-namespace" 38 workloadName = "test-workload-name" 39 workloadUID = "test-workload-uid" 40 traitName = "test-trait-name" 41 deploymentName = "test-deployment-name" 42 workloadDefinitionNamespace = "containerizedworkloads.core.oam.dev" 43 serverErr = "server error" 44 ) 45 46 func TestReconcilerSetupWithManager(t *testing.T) { 47 assert := asserts.New(t) 48 49 var mocker *gomock.Controller 50 var mgr *mocks.MockManager 51 var cli *mocks.MockClient 52 var scheme *runtime.Scheme 53 54 var reconciler LoggingTraitReconciler 55 var err error 56 57 mocker = gomock.NewController(t) 58 mgr = mocks.NewMockManager(mocker) 59 cli = mocks.NewMockClient(mocker) 60 scheme = runtime.NewScheme() 61 _ = vzapi.AddToScheme(scheme) 62 reconciler = LoggingTraitReconciler{Client: cli, Scheme: scheme} 63 mgr.EXPECT().GetControllerOptions().AnyTimes() 64 mgr.EXPECT().GetScheme().Return(scheme) 65 mgr.EXPECT().GetLogger().Return(logr.Discard()) 66 mgr.EXPECT().SetFields(gomock.Any()).Return(nil).AnyTimes() 67 mgr.EXPECT().Add(gomock.Any()).Return(nil).AnyTimes() 68 err = reconciler.SetupWithManager(mgr) 69 mocker.Finish() 70 assert.NoError(err) 71 } 72 73 // TestLoggingTraitCreatedForContainerizedWorkload tests the creation of a logging trait related to a containerized workload. 74 // GIVEN a logging trait that has been created 75 // AND the logging trait is related to a containerized workload 76 // WHEN the logging trait Reconcile method is invoked 77 // THEN verify that logging trait finalizer is added 78 // AND verify that pod annotations are updated 79 // AND verify that the scraper configmap is updated 80 // AND verify that the scraper pod is restarted 81 func TestLoggingTraitCreatedForContainerizedWorkload(t *testing.T) { 82 assert := asserts.New(t) 83 mocker := gomock.NewController(t) 84 mock := mocks.NewMockClient(mocker) 85 mockStatus := mocks.NewMockStatusWriter(mocker) 86 87 testDeployment := newDeployment(deploymentName, namespaceName, workloadName, workloadUID) 88 89 // Expect a call to get the logging trait 90 mock.EXPECT(). 91 Get(gomock.Any(), gomock.Eq(types.NamespacedName{Namespace: namespaceName, Name: traitName}), gomock.Not(gomock.Nil()), gomock.Any()). 92 DoAndReturn(func(ctx context.Context, name types.NamespacedName, trait *vzapi.LoggingTrait, opt ...client.GetOption) error { 93 trait.SetWorkloadReference(oamrt.TypedReference{ 94 APIVersion: oamcore.SchemeGroupVersion.Identifier(), 95 Kind: oamcore.ContainerizedWorkloadKind, 96 Name: workloadName, 97 UID: types.UID(workloadUID), 98 }) 99 trait.SetNamespace(namespaceName) 100 return nil 101 }) 102 // Expect a call to get the workload 103 mock.EXPECT(). 104 Get(gomock.Any(), gomock.Eq(client.ObjectKey{Namespace: namespaceName, Name: workloadName}), gomock.Not(gomock.Nil()), gomock.Any()). 105 DoAndReturn(func(ctx context.Context, key client.ObjectKey, workload *unstructured.Unstructured, opt ...client.GetOption) error { 106 return nil 107 }) 108 // Expect a call to get the workload definition 109 mock.EXPECT(). 110 Get(gomock.Any(), gomock.Eq(types.NamespacedName{Namespace: "", Name: workloadDefinitionNamespace}), gomock.Not(gomock.Nil()), gomock.Any()). 111 DoAndReturn(func(ctx context.Context, key client.ObjectKey, workloadDef *oamcore.WorkloadDefinition, opt ...client.GetOption) error { 112 workloadDef.Spec.ChildResourceKinds = []oamcore.ChildResourceKind{ 113 { 114 APIVersion: k8sapps.SchemeGroupVersion.Identifier(), 115 Kind: "Deployment", 116 }, 117 } 118 return nil 119 }) 120 // Expect to list config map 121 options := []client.ListOption{client.InNamespace(namespaceName), client.MatchingFields{"metadata.name": "logging-stdout-test-deployment-name-deployment"}} 122 mock.EXPECT(). 123 List(gomock.Any(), gomock.Not(gomock.Nil()), options). 124 DoAndReturn(func(ctx context.Context, list *unstructured.UnstructuredList, opts ...client.ListOption) error { 125 return nil 126 }) 127 // Expect to create a config map 128 mock.EXPECT(). 129 Create(gomock.Any(), gomock.Not(gomock.Nil()), gomock.Any()). 130 DoAndReturn(func(ctx context.Context, configMap *corev1.ConfigMap, opts ...client.CreateOption) error { 131 return nil 132 }) 133 // Expect a call to get the deployment 134 mock.EXPECT(). 135 Get(gomock.Any(), gomock.Eq(client.ObjectKey{Namespace: namespaceName, Name: deploymentName}), gomock.Not(gomock.Nil()), gomock.Any()). 136 DoAndReturn(func(ctx context.Context, key client.ObjectKey, workload *unstructured.Unstructured, opt ...client.GetOption) error { 137 return nil 138 }) 139 // Expect a call to list the child Deployment resources of the containerized workload definition 140 mock.EXPECT(). 141 List(gomock.Any(), gomock.Not(gomock.Nil()), gomock.Any()). 142 DoAndReturn(func(ctx context.Context, list *unstructured.UnstructuredList, opts ...client.ListOption) error { 143 assert.Equal("Deployment", list.GetKind()) 144 return appendAsUnstructured(list, testDeployment) 145 }) 146 // Expect a call to get the status writer 147 mock.EXPECT().Status().Return(mockStatus).AnyTimes() 148 149 // Create and make the request 150 request := ctrl.Request{NamespacedName: types.NamespacedName{Namespace: namespaceName, Name: "test-trait-name"}} 151 152 reconciler := newLoggingTraitReconciler(mock, t) 153 result, err := reconciler.Reconcile(context.TODO(), request) 154 155 // Validate the results 156 mocker.Finish() 157 assert.NoError(err) 158 assert.Equal(time.Duration(0), result.RequeueAfter) 159 } 160 161 // TestDeleteLoggingTraitFromContainerizedWorkload tests the deletion of a logging trait related to a containerized workload. 162 // GIVEN a logging trait 163 // AND the logging trait is related to a containerized workload 164 // WHEN the logging trait reconcileTraitDelete method is invoked 165 // THEN verify that the logging trait has been deleted 166 func TestDeleteLoggingTraitFromContainerizedWorkload(t *testing.T) { 167 assert := asserts.New(t) 168 mocker := gomock.NewController(t) 169 mock := mocks.NewMockClient(mocker) 170 171 testDeployment := newDeployment(deploymentName, namespaceName, workloadName, workloadUID) 172 173 // Create trait for deletion 174 trait := vzapi.LoggingTrait{ 175 TypeMeta: k8smeta.TypeMeta{ 176 Kind: vzapi.LoggingTraitKind, 177 }, 178 ObjectMeta: k8smeta.ObjectMeta{ 179 Name: traitName, 180 Namespace: namespaceName, 181 }, 182 Spec: vzapi.LoggingTraitSpec{ 183 WorkloadReference: oamrt.TypedReference{ 184 APIVersion: oamcore.SchemeGroupVersion.Identifier(), 185 Kind: oamcore.ContainerizedWorkloadKind, 186 Name: workloadName, 187 UID: types.UID(workloadUID), 188 }, 189 }, 190 Status: vzapi.LoggingTraitStatus{}, 191 } 192 193 // Expect a call to get the workload 194 mock.EXPECT(). 195 Get(gomock.Any(), gomock.Eq(types.NamespacedName{Namespace: namespaceName, Name: workloadName}), gomock.Not(gomock.Nil()), gomock.Any()). 196 DoAndReturn(func(ctx context.Context, name types.NamespacedName, workload *unstructured.Unstructured, opt ...client.GetOption) error { 197 workload.SetNamespace(namespaceName) 198 return nil 199 }) 200 // Expect a call to get the workload definition 201 mock.EXPECT(). 202 Get(gomock.Any(), gomock.Eq(types.NamespacedName{Namespace: "", Name: workloadDefinitionNamespace}), gomock.Not(gomock.Nil()), gomock.Any()). 203 DoAndReturn(func(ctx context.Context, key client.ObjectKey, workloadDef *oamcore.WorkloadDefinition, opt ...client.GetOption) error { 204 workloadDef.Spec.ChildResourceKinds = []oamcore.ChildResourceKind{ 205 { 206 APIVersion: k8sapps.SchemeGroupVersion.Identifier(), 207 Kind: "Deployment", 208 }, 209 } 210 return nil 211 }) 212 // Expect to list deployment 213 options := []client.ListOption{client.InNamespace(namespaceName)} 214 mock.EXPECT(). 215 List(gomock.Any(), gomock.Not(gomock.Nil()), options). 216 DoAndReturn(func(ctx context.Context, deployment *unstructured.UnstructuredList, opts ...client.ListOption) error { 217 unstructuredDeployment, err := convertToUnstructured(testDeployment) 218 if err != nil { 219 t.Fatalf("Could not create unstructured Deployment") 220 } 221 deployment.Items = []unstructured.Unstructured{unstructuredDeployment} 222 return nil 223 }) 224 // Expect a call to get the deployment 225 mock.EXPECT(). 226 Get(gomock.Any(), gomock.Eq(client.ObjectKey{Namespace: namespaceName, Name: deploymentName}), gomock.Not(gomock.Nil()), gomock.Any()). 227 DoAndReturn(func(ctx context.Context, key client.ObjectKey, workload *unstructured.Unstructured, opt ...client.GetOption) error { 228 return nil 229 }) 230 // Expect to list config map 231 mock.EXPECT(). 232 List(gomock.Any(), gomock.Not(gomock.Nil()), client.InNamespace(namespaceName), client.MatchingFields{"metadata.name": "logging-stdout-test-deployment-name-deployment"}). 233 DoAndReturn(func(ctx context.Context, list *unstructured.UnstructuredList, options ...client.ListOption) error { 234 return nil 235 }) 236 237 reconciler := newLoggingTraitReconciler(mock, t) 238 result, err := reconciler.reconcileTraitDelete(context.TODO(), vzlog.DefaultLogger(), &trait) 239 240 // Validate the results 241 mocker.Finish() 242 assert.NoError(err) 243 assert.Equal(time.Duration(0), result.RequeueAfter) 244 } 245 246 // Test_fetchTrait tests the fetchTrait function of the LoggingTraitReconciler 247 // GIVEN a call to fetchTrait method of the LoggingTrait Reconciler 248 // WHEN there is some error during retrieving the trait 249 // THEN expect the reconciler to requeue and return no error 250 func TestFetchTraitError(t *testing.T) { 251 assert := asserts.New(t) 252 mocker := gomock.NewController(t) 253 mock := mocks.NewMockClient(mocker) 254 255 request := ctrl.Request{NamespacedName: types.NamespacedName{Namespace: namespaceName, Name: traitName}} 256 257 mock.EXPECT(). 258 Get(gomock.Any(), gomock.Eq(types.NamespacedName{Namespace: namespaceName, Name: traitName}), gomock.Not(gomock.Nil()), gomock.Any()). 259 Return( 260 fmt.Errorf(serverErr), 261 ) 262 263 reconciler := newLoggingTraitReconciler(mock, t) 264 result, err := reconciler.Reconcile(context.TODO(), request) 265 assert.Nil(err) 266 assert.NotNil(result) 267 assert.True(result.Requeue) 268 269 mock = mocks.NewMockClient(mocker) 270 mock.EXPECT(). 271 Get(gomock.Any(), gomock.Eq(types.NamespacedName{Namespace: namespaceName, Name: traitName}), gomock.Not(gomock.Nil()), gomock.Any()). 272 Return( 273 &errors.StatusError{ErrStatus: k8smeta.Status{Code: 404}}, 274 ) 275 276 reconciler = newLoggingTraitReconciler(mock, t) 277 result, err = reconciler.Reconcile(context.TODO(), request) 278 assert.Nil(err) 279 assert.NotNil(result) 280 assert.True(result.Requeue) 281 } 282 283 // TestCreateOrUpdateLoggingTraitNoWorkloadChild tests the creation/update/deletion of LoggingTrait when 284 // no child resources for workload exists 285 // GIVEN a LoggingTrait with workload reference 286 // WHEN there is no child resource of the workload 287 // THEN fall back to the original workload and complete the reconciliation 288 func TestReconcileTraitNoWorkloadChild(t *testing.T) { 289 assert := asserts.New(t) 290 mocker := gomock.NewController(t) 291 mock := mocks.NewMockClient(mocker) 292 293 trait := &vzapi.LoggingTrait{ 294 TypeMeta: k8smeta.TypeMeta{ 295 Kind: vzapi.LoggingTraitKind, 296 }, 297 ObjectMeta: k8smeta.ObjectMeta{ 298 Name: traitName, 299 Namespace: namespaceName, 300 }, 301 Spec: vzapi.LoggingTraitSpec{ 302 WorkloadReference: oamrt.TypedReference{ 303 APIVersion: oamcore.SchemeGroupVersion.Identifier(), 304 Kind: oamcore.ContainerizedWorkloadKind, 305 Name: workloadName, 306 UID: types.UID(workloadUID), 307 }}, 308 } 309 310 mock.EXPECT(). 311 Get(gomock.Any(), gomock.Eq(client.ObjectKey{Namespace: namespaceName, Name: workloadName}), gomock.Not(gomock.Nil()), gomock.Any()). 312 DoAndReturn(func(ctx context.Context, key client.ObjectKey, obj *unstructured.Unstructured, opt ...client.GetOption) error { 313 return nil 314 }) 315 316 mock.EXPECT(). 317 Get(gomock.Any(), gomock.Eq(client.ObjectKey{Namespace: "", Name: workloadDefinitionNamespace}), gomock.Not(gomock.Nil()), gomock.Any()). 318 Return(fmt.Errorf(serverErr)) 319 320 reconciler := newLoggingTraitReconciler(mock, t) 321 result, supported, err := reconciler.reconcileTraitCreateOrUpdate(context.TODO(), vzlog.DefaultLogger(), trait) 322 assert.NoError(err) 323 assert.NotNil(result) 324 assert.True(supported) 325 326 mock = mocks.NewMockClient(mocker) 327 mock.EXPECT(). 328 Get(gomock.Any(), gomock.Eq(client.ObjectKey{Namespace: namespaceName, Name: workloadName}), gomock.Not(gomock.Nil()), gomock.Any()). 329 DoAndReturn(func(ctx context.Context, key client.ObjectKey, obj *unstructured.Unstructured, opt ...client.GetOption) error { 330 return nil 331 }) 332 333 mock.EXPECT(). 334 Get(gomock.Any(), gomock.Eq(client.ObjectKey{Namespace: "", Name: workloadDefinitionNamespace}), gomock.Not(gomock.Nil()), gomock.Any()). 335 Return(fmt.Errorf(serverErr)) 336 337 reconciler = newLoggingTraitReconciler(mock, t) 338 result, err = reconciler.reconcileTraitDelete(context.TODO(), vzlog.DefaultLogger(), trait) 339 assert.NoError(err) 340 assert.NotNil(result) 341 } 342 343 // convertToUnstructured converts an object to an Unstructured version 344 // object - The object to convert to Unstructured 345 func convertToUnstructured(object interface{}) (unstructured.Unstructured, error) { 346 jbytes, err := json.Marshal(object) 347 if err != nil { 348 return unstructured.Unstructured{}, err 349 } 350 var u map[string]interface{} 351 _ = json.Unmarshal(jbytes, &u) 352 return unstructured.Unstructured{Object: u}, nil 353 } 354 355 // appendAsUnstructured appends an object to the list after converting it to an Unstructured 356 // list - The list to append to. 357 // object - The object to convert to Unstructured and append to the list 358 func appendAsUnstructured(list *unstructured.UnstructuredList, object interface{}) error { 359 u, err := convertToUnstructured(object) 360 if err != nil { 361 return err 362 } 363 list.Items = append(list.Items, u) 364 return nil 365 } 366 367 // newLoggingTraitReconciler creates a new reconciler for testing 368 // cli - The Kerberos client to inject into the reconciler 369 func newLoggingTraitReconciler(cli client.Client, t *testing.T) LoggingTraitReconciler { 370 scheme := runtime.NewScheme() 371 vzapi.AddToScheme(scheme) 372 reconciler := LoggingTraitReconciler{ 373 Client: cli, 374 Log: zap.S(), 375 Scheme: scheme, 376 } 377 return reconciler 378 } 379 380 func newDeployment(deploymentName string, namespaceName string, workloadName string, workloadUID string) k8sapps.Deployment { 381 return k8sapps.Deployment{ 382 TypeMeta: k8smeta.TypeMeta{ 383 APIVersion: k8sapps.SchemeGroupVersion.Identifier(), 384 Kind: "Deployment", 385 }, 386 ObjectMeta: k8smeta.ObjectMeta{ 387 Name: deploymentName, 388 Namespace: namespaceName, 389 CreationTimestamp: k8smeta.Now(), 390 OwnerReferences: []k8smeta.OwnerReference{ 391 { 392 APIVersion: oamcore.SchemeGroupVersion.Identifier(), 393 Kind: oamcore.ContainerizedWorkloadKind, 394 Name: workloadName, 395 UID: types.UID(workloadUID), 396 }, 397 }, 398 }, 399 Spec: k8sapps.DeploymentSpec{ 400 Template: corev1.PodTemplateSpec{ 401 Spec: corev1.PodSpec{ 402 Containers: []corev1.Container{ 403 { 404 Name: "test-container", 405 }, 406 }, 407 }, 408 }, 409 }, 410 } 411 } 412 413 // TestReconcileKubeSystem tests to make sure we do not reconcile 414 // Any resource that belong to the kube-system namespace 415 func TestReconcileKubeSystem(t *testing.T) { 416 assert := asserts.New(t) 417 mocker := gomock.NewController(t) 418 mock := mocks.NewMockClient(mocker) 419 420 // create a request and reconcile it 421 request := ctrl.Request{NamespacedName: types.NamespacedName{Namespace: vzconst.KubeSystem, Name: traitName}} 422 reconciler := newLoggingTraitReconciler(mock, t) 423 result, err := reconciler.Reconcile(context.TODO(), request) 424 425 // Validate the results 426 mocker.Finish() 427 assert.Nil(err) 428 assert.True(result.IsZero()) 429 }