github.com/verrazzano/verrazzano@v1.7.0/application-operator/controllers/helidonworkload/helidonworkload_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 helidonworkload 5 6 import ( 7 "context" 8 "os" 9 "strconv" 10 "strings" 11 "testing" 12 13 "github.com/verrazzano/verrazzano/application-operator/controllers/clusters" 14 15 "github.com/go-logr/logr" 16 "github.com/prometheus/client_golang/prometheus/testutil" 17 vzconst "github.com/verrazzano/verrazzano/pkg/constants" 18 19 oamapi "github.com/crossplane/oam-kubernetes-runtime/apis/core/v1alpha2" 20 "github.com/crossplane/oam-kubernetes-runtime/pkg/oam" 21 "github.com/golang/mock/gomock" 22 asserts "github.com/stretchr/testify/assert" 23 vzapi "github.com/verrazzano/verrazzano/application-operator/apis/oam/v1alpha1" 24 "github.com/verrazzano/verrazzano/application-operator/controllers/logging" 25 "github.com/verrazzano/verrazzano/application-operator/controllers/metricstrait" 26 "github.com/verrazzano/verrazzano/application-operator/metricsexporter" 27 "github.com/verrazzano/verrazzano/application-operator/mocks" 28 "go.uber.org/zap" 29 appsv1 "k8s.io/api/apps/v1" 30 corev1 "k8s.io/api/core/v1" 31 k8serrors "k8s.io/apimachinery/pkg/api/errors" 32 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 34 "k8s.io/apimachinery/pkg/runtime" 35 k8sschema "k8s.io/apimachinery/pkg/runtime/schema" 36 "k8s.io/apimachinery/pkg/types" 37 "k8s.io/apimachinery/pkg/util/intstr" 38 k8scheme "k8s.io/client-go/kubernetes/scheme" 39 ctrl "sigs.k8s.io/controller-runtime" 40 "sigs.k8s.io/controller-runtime/pkg/client" 41 "sigs.k8s.io/controller-runtime/pkg/client/fake" 42 "sigs.k8s.io/yaml" 43 ) 44 45 const ( 46 namespace = "unit-test-namespace" 47 testRestartVersion = "new-restart" 48 helidonWorkload = "testdata/templates/helidon_workload.yaml" 49 ) 50 51 // TestReconcilerSetupWithManager test the creation of the VerrazzanoHelidonWorkload reconciler. 52 // GIVEN a controller implementation 53 // WHEN the controller is created 54 // THEN verify no error is returned 55 func TestReconcilerSetupWithManager(t *testing.T) { 56 assert := asserts.New(t) 57 58 var mocker *gomock.Controller 59 var mgr *mocks.MockManager 60 var cli *mocks.MockClient 61 var scheme *runtime.Scheme 62 var reconciler Reconciler 63 var err error 64 65 mocker = gomock.NewController(t) 66 mgr = mocks.NewMockManager(mocker) 67 cli = mocks.NewMockClient(mocker) 68 scheme = runtime.NewScheme() 69 _ = vzapi.AddToScheme(scheme) 70 reconciler = Reconciler{Client: cli, Scheme: scheme} 71 mgr.EXPECT().GetControllerOptions().AnyTimes() 72 mgr.EXPECT().GetScheme().Return(scheme) 73 mgr.EXPECT().GetLogger().Return(logr.Discard()) 74 mgr.EXPECT().SetFields(gomock.Any()).Return(nil).AnyTimes() 75 mgr.EXPECT().Add(gomock.Any()).Return(nil).AnyTimes() 76 err = reconciler.SetupWithManager(mgr) 77 mocker.Finish() 78 assert.NoError(err) 79 } 80 81 // TestReconcileWorkloadNotFound tests reconciling a VerrazzanoHelidonWorkload when the workload 82 // cannot be fetched. This happens when the workload has been deleted by the OAM runtime. 83 // GIVEN a VerrazzanoHelidonWorkload resource has been deleted 84 // WHEN the controller Reconcile function is called and we attempt to fetch the workload 85 // THEN return success from the controller as there is nothing more to do 86 func TestReconcileWorkloadNotFound(t *testing.T) { 87 88 assert := asserts.New(t) 89 90 var mocker = gomock.NewController(t) 91 var cli = mocks.NewMockClient(mocker) 92 93 // expect a call to fetch the VerrazzanoHelidonWorkload 94 cli.EXPECT(). 95 Get(gomock.Any(), types.NamespacedName{Namespace: namespace, Name: "unit-test-verrazzano-helidon-workload"}, gomock.Not(gomock.Nil()), gomock.Any()). 96 DoAndReturn(func(ctx context.Context, name types.NamespacedName, workload *vzapi.VerrazzanoHelidonWorkload, opt ...client.GetOption) error { 97 return k8serrors.NewNotFound(k8sschema.GroupResource{}, "") 98 }) 99 100 // create a request and reconcile it 101 request := newRequest(namespace, "unit-test-verrazzano-helidon-workload") 102 reconciler := newReconciler(cli) 103 result, err := reconciler.Reconcile(context.TODO(), request) 104 105 mocker.Finish() 106 assert.NoError(err) 107 assert.Equal(false, result.Requeue) 108 } 109 110 // TestReconcileFetchWorkloadError tests reconciling a VerrazzanoHelidonWorkload when the workload 111 // cannot be fetched due to an unexpected error. 112 // GIVEN a VerrazzanoHelidonWorkload resource has been created 113 // WHEN the controller Reconcile function is called and we attempt to fetch the workload and get an error 114 // THEN return the error 115 func TestReconcileFetchWorkloadError(t *testing.T) { 116 117 assert := asserts.New(t) 118 119 var mocker = gomock.NewController(t) 120 var cli = mocks.NewMockClient(mocker) 121 122 // expect a call to fetch the VerrazzanoHelidonWorkload 123 cli.EXPECT(). 124 Get(gomock.Any(), types.NamespacedName{Namespace: namespace, Name: "unit-test-verrazzano-helidon-workload"}, gomock.Not(gomock.Nil()), gomock.Any()). 125 DoAndReturn(func(ctx context.Context, name types.NamespacedName, workload *vzapi.VerrazzanoHelidonWorkload, opt ...client.GetOption) error { 126 return k8serrors.NewBadRequest("An error has occurred") 127 }) 128 129 // create a request and reconcile it 130 request := newRequest(namespace, "unit-test-verrazzano-helidon-workload") 131 reconciler := newReconciler(cli) 132 result, err := reconciler.Reconcile(context.TODO(), request) 133 134 mocker.Finish() 135 assert.Nil(err) 136 assert.True(result.Requeue) 137 } 138 139 // TestReconcileWorkloadMissingData tests reconciling a VerrazzanoHelidonWorkload when the workload 140 // can be fetched but doesn't contain all required data. 141 // GIVEN a VerrazzanoHelidonWorkload resource has been created 142 // WHEN the controller Reconcile function is called and we attempt to validate the workload and get an error 143 // THEN return the error 144 func TestReconcileWorkloadMissingData(t *testing.T) { 145 146 assert := asserts.New(t) 147 var mocker = gomock.NewController(t) 148 var cli = mocks.NewMockClient(mocker) 149 150 appConfigName := "unit-test-app-config" 151 componentName := "unit-test-component" 152 labels := map[string]string{oam.LabelAppComponent: componentName, oam.LabelAppName: appConfigName} 153 helidonTestContainerPort := corev1.ContainerPort{ 154 ContainerPort: 8080, 155 Name: "http", 156 } 157 helidonTestContainer := corev1.Container{ 158 Name: "hello-helidon-container-new", 159 Image: "ghcr.io/verrazzano/example-helidon-greet-app-v1:1.0.0-1-20211215184123-0a1b633", 160 Ports: []corev1.ContainerPort{ 161 helidonTestContainerPort, 162 }, 163 } 164 deploymentTemplate := &vzapi.DeploymentTemplate{ 165 Metadata: metav1.ObjectMeta{ 166 Namespace: namespace, 167 Labels: map[string]string{ 168 "app": "hello-helidon-deploy-new", 169 }, 170 }, 171 PodSpec: corev1.PodSpec{ 172 Containers: []corev1.Container{ 173 helidonTestContainer, 174 }, 175 }, 176 } 177 178 // expect a call to fetch the VerrazzanoHelidonWorkload 179 cli.EXPECT(). 180 Get(gomock.Any(), types.NamespacedName{Namespace: namespace, Name: "unit-test-verrazzano-helidon-workload"}, gomock.Not(gomock.Nil()), gomock.Any()). 181 DoAndReturn(func(ctx context.Context, name types.NamespacedName, workload *vzapi.VerrazzanoHelidonWorkload, opt ...client.GetOption) error { 182 workload.Spec.DeploymentTemplate = *deploymentTemplate 183 workload.ObjectMeta.Labels = labels 184 workload.APIVersion = vzapi.SchemeGroupVersion.String() 185 workload.Kind = "VerrazzanoHelidonWorkload" 186 workload.Namespace = namespace 187 return nil 188 }) 189 190 // create a request and reconcile it 191 request := newRequest(namespace, "unit-test-verrazzano-helidon-workload") 192 reconciler := newReconciler(cli) 193 result, err := reconciler.Reconcile(context.TODO(), request) 194 195 mocker.Finish() 196 assert.Nil(err) 197 assert.True(result.Requeue) 198 } 199 200 // TestReconcileCreateHelidon tests the basic happy path of reconciling a VerrazzanoHelidonWorkload. We 201 // expect to write out a Deployment and Service but we aren't adding logging or any other scopes or traits. 202 // GIVEN a VerrazzanoHelidonWorkload resource is created 203 // WHEN the controller Reconcile function is called 204 // THEN expect a Deployment and Service to be written 205 func TestReconcileCreateHelidon(t *testing.T) { 206 207 assert := asserts.New(t) 208 var mocker = gomock.NewController(t) 209 var cli = mocks.NewMockClient(mocker) 210 mockStatus := mocks.NewMockStatusWriter(mocker) 211 212 appConfigName := "unit-test-app-config" 213 componentName := "unit-test-component" 214 labels := map[string]string{oam.LabelAppComponent: componentName, oam.LabelAppName: appConfigName} 215 helidonTestContainerPort := corev1.ContainerPort{ 216 ContainerPort: 8080, 217 Name: "http", 218 } 219 helidonTestContainer := corev1.Container{ 220 Name: "hello-helidon-container-new", 221 Image: "ghcr.io/verrazzano/example-helidon-greet-app-v1:1.0.0-1-20211215184123-0a1b633", 222 Ports: []corev1.ContainerPort{ 223 helidonTestContainerPort, 224 }, 225 } 226 227 deploymentTemplate := &vzapi.DeploymentTemplate{ 228 Metadata: metav1.ObjectMeta{ 229 Name: "hello-helidon-deployment-new", 230 Namespace: namespace, 231 Labels: map[string]string{ 232 "app": "hello-helidon-deploy-new", 233 }, 234 }, 235 PodSpec: corev1.PodSpec{ 236 Containers: []corev1.Container{ 237 helidonTestContainer, 238 }, 239 }, 240 Selector: metav1.LabelSelector{ 241 MatchLabels: map[string]string{ 242 "app": "hello-helidon", 243 }, 244 MatchExpressions: []metav1.LabelSelectorRequirement{{ 245 Key: "app", 246 Operator: "In", 247 Values: []string{"hello-helidon"}, 248 }}, 249 }, 250 } 251 // expect call to fetch existing deployment 252 cli.EXPECT(). 253 Get(gomock.Any(), types.NamespacedName{Namespace: namespace, Name: "hello-helidon-deployment-new"}, gomock.Not(gomock.Nil()), gomock.Any()). 254 DoAndReturn(func(ctx context.Context, name types.NamespacedName, deployment *appsv1.Deployment, opt ...client.GetOption) error { 255 return k8serrors.NewNotFound(k8sschema.GroupResource{}, "test") 256 }) 257 // expect a call to fetch the application configuration 258 cli.EXPECT(). 259 Get(gomock.Any(), types.NamespacedName{Namespace: namespace, Name: "unit-test-app-config"}, gomock.Not(gomock.Nil()), gomock.Any()). 260 DoAndReturn(func(ctx context.Context, name types.NamespacedName, appconf *oamapi.ApplicationConfiguration, opt ...client.GetOption) error { 261 appconf.Namespace = name.Namespace 262 appconf.Name = name.Name 263 appconf.APIVersion = oamapi.SchemeGroupVersion.String() 264 appconf.Kind = oamapi.ApplicationConfigurationKind 265 appconf.Spec.Components = []oamapi.ApplicationConfigurationComponent{{ComponentName: "unit-test-component"}} 266 return nil 267 }) 268 // expect a call to fetch the VerrazzanoHelidonWorkload 269 cli.EXPECT(). 270 Get(gomock.Any(), types.NamespacedName{Namespace: namespace, Name: "unit-test-verrazzano-helidon-workload"}, gomock.Not(gomock.Nil()), gomock.Any()). 271 DoAndReturn(func(ctx context.Context, name types.NamespacedName, workload *vzapi.VerrazzanoHelidonWorkload, opt ...client.GetOption) error { 272 workload.Spec.DeploymentTemplate = *deploymentTemplate 273 workload.ObjectMeta.Labels = labels 274 workload.APIVersion = vzapi.SchemeGroupVersion.String() 275 workload.Kind = "VerrazzanoHelidonWorkload" 276 workload.Namespace = namespace 277 return nil 278 }) 279 // expect a call to create the Deployment 280 cli.EXPECT(). 281 Patch(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). 282 DoAndReturn(func(ctx context.Context, deploy *appsv1.Deployment, patch client.Patch, applyOpts ...client.PatchOption) error { 283 assert.Equal(deploymentAPIVersion, deploy.APIVersion) 284 assert.Equal(deploymentKind, deploy.Kind) 285 // make sure the OAM component and app name labels were copied 286 expectedLabels := map[string]string{ 287 "app": "hello-helidon-deploy-new", 288 oam.LabelAppName: "unit-test-app-config", 289 oam.LabelAppComponent: "unit-test-component"} 290 assert.Equal(expectedLabels, deploy.GetLabels()) 291 assert.Equal([]corev1.Container{ 292 helidonTestContainer, 293 }, deploy.Spec.Template.Spec.Containers) 294 return nil 295 }) 296 // expect a call to create the Service 297 cli.EXPECT(). 298 Patch(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). 299 DoAndReturn(func(ctx context.Context, service *corev1.Service, patch client.Patch, applyOpts ...client.PatchOption) error { 300 // make sure the OAM component and app name labels were copied 301 assert.Equal("unit-test-app-config", service.Labels[oam.LabelAppName]) 302 assert.Equal("unit-test-component", service.Labels[oam.LabelAppComponent]) 303 assert.Equal(serviceAPIVersion, service.APIVersion) 304 assert.Equal(serviceKind, service.Kind) 305 return nil 306 }) 307 // expect a call to status update 308 cli.EXPECT().Status().Return(mockStatus).AnyTimes() 309 mockStatus.EXPECT(). 310 Update(gomock.Any(), gomock.Any(), gomock.Any()). 311 DoAndReturn(func(ctx context.Context, workload *vzapi.VerrazzanoHelidonWorkload, opts ...client.UpdateOption) error { 312 assert.Len(workload.Status.Resources, 2) 313 return nil 314 }) 315 316 // create a request and reconcile it 317 request := newRequest(namespace, "unit-test-verrazzano-helidon-workload") 318 reconciler := newReconciler(cli) 319 result, err := reconciler.Reconcile(context.TODO(), request) 320 321 mocker.Finish() 322 assert.NoError(err) 323 assert.Equal(false, result.Requeue) 324 } 325 326 // TestReconcileCreateHelidonWithServiceTemplate tests the basic happy path of reconciling a VerrazzanoHelidonWorkload. We 327 // expect to write out a Deployment and Service from the but we aren't adding logging or any other scopes or traits. 328 // GIVEN a VerrazzanoHelidonWorkload with a ServiceTemplate 329 // WHEN the controller Reconcile function is called 330 // THEN expect a Deployment and Service to be written using the ServiceTemplate 331 func TestReconcileCreateHelidonWithServiceTemplate(t *testing.T) { 332 333 assert := asserts.New(t) 334 var mocker = gomock.NewController(t) 335 var cli = mocks.NewMockClient(mocker) 336 mockStatus := mocks.NewMockStatusWriter(mocker) 337 338 appConfigName := "unit-test-app-config" 339 componentName := "unit-test-component" 340 labels := map[string]string{oam.LabelAppComponent: componentName, oam.LabelAppName: appConfigName} 341 helidonTestContainerPort := corev1.ContainerPort{ 342 ContainerPort: 8080, 343 Name: "http", 344 } 345 helidonTestContainer := corev1.Container{ 346 Name: "hello-helidon-container-new", 347 Image: "ghcr.io/verrazzano/example-helidon-greet-app-v1:1.0.0-1-20211215184123-0a1b633", 348 Ports: []corev1.ContainerPort{ 349 helidonTestContainerPort, 350 }, 351 } 352 testServicePort := corev1.ServicePort{ 353 Name: "test-port", 354 Port: 1111, 355 TargetPort: intstr.FromInt(2222), 356 Protocol: corev1.ProtocolTCP, 357 } 358 359 deploymentTemplate := &vzapi.DeploymentTemplate{ 360 Metadata: metav1.ObjectMeta{ 361 Name: "hello-helidon-deployment-new", 362 Namespace: namespace, 363 Labels: map[string]string{ 364 "app": "hello-helidon-deploy-new", 365 }, 366 }, 367 PodSpec: corev1.PodSpec{ 368 Containers: []corev1.Container{ 369 helidonTestContainer, 370 }, 371 }, 372 Selector: metav1.LabelSelector{ 373 MatchLabels: map[string]string{ 374 "app": "hello-helidon", 375 }, 376 MatchExpressions: []metav1.LabelSelectorRequirement{{ 377 Key: "app", 378 Operator: "In", 379 Values: []string{"hello-helidon"}, 380 }}, 381 }, 382 } 383 384 serviceTemplate := &vzapi.ServiceTemplate{ 385 Metadata: metav1.ObjectMeta{ 386 Labels: map[string]string{"test-key": "test-val"}, 387 Annotations: map[string]string{"test-key": "test-val"}, 388 }, 389 ServiceSpec: corev1.ServiceSpec{ 390 Ports: []corev1.ServicePort{testServicePort}, 391 }, 392 } 393 394 // expect call to fetch existing deployment 395 cli.EXPECT(). 396 Get(gomock.Any(), types.NamespacedName{Namespace: namespace, Name: "hello-helidon-deployment-new"}, gomock.Not(gomock.Nil()), gomock.Any()). 397 DoAndReturn(func(ctx context.Context, name types.NamespacedName, deployment *appsv1.Deployment, opt ...client.GetOption) error { 398 return k8serrors.NewNotFound(k8sschema.GroupResource{}, "test") 399 }) 400 // expect a call to fetch the application configuration 401 cli.EXPECT(). 402 Get(gomock.Any(), types.NamespacedName{Namespace: namespace, Name: "unit-test-app-config"}, gomock.Not(gomock.Nil()), gomock.Any()). 403 DoAndReturn(func(ctx context.Context, name types.NamespacedName, appconf *oamapi.ApplicationConfiguration, opt ...client.GetOption) error { 404 appconf.Namespace = name.Namespace 405 appconf.Name = name.Name 406 appconf.APIVersion = oamapi.SchemeGroupVersion.String() 407 appconf.Kind = oamapi.ApplicationConfigurationKind 408 appconf.Spec.Components = []oamapi.ApplicationConfigurationComponent{{ComponentName: "unit-test-component"}} 409 return nil 410 }) 411 // expect a call to fetch the VerrazzanoHelidonWorkload 412 cli.EXPECT(). 413 Get(gomock.Any(), types.NamespacedName{Namespace: namespace, Name: "unit-test-verrazzano-helidon-workload"}, gomock.Not(gomock.Nil()), gomock.Any()). 414 DoAndReturn(func(ctx context.Context, name types.NamespacedName, workload *vzapi.VerrazzanoHelidonWorkload, opt ...client.GetOption) error { 415 workload.Spec.DeploymentTemplate = *deploymentTemplate 416 workload.Spec.ServiceTemplate = *serviceTemplate 417 workload.ObjectMeta.Labels = labels 418 workload.APIVersion = vzapi.SchemeGroupVersion.String() 419 workload.Kind = "VerrazzanoHelidonWorkload" 420 workload.Namespace = namespace 421 return nil 422 }) 423 // expect a call to create the Deployment 424 cli.EXPECT(). 425 Patch(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). 426 DoAndReturn(func(ctx context.Context, deploy *appsv1.Deployment, patch client.Patch, applyOpts ...client.PatchOption) error { 427 assert.Equal(deploymentAPIVersion, deploy.APIVersion) 428 assert.Equal(deploymentKind, deploy.Kind) 429 // make sure the OAM component and app name labels were copied 430 assert.Equal(map[string]string{"app": "hello-helidon-deploy-new", "app.oam.dev/component": "unit-test-component", "app.oam.dev/name": "unit-test-app-config"}, deploy.GetLabels()) 431 assert.Equal([]corev1.Container{ 432 helidonTestContainer, 433 }, deploy.Spec.Template.Spec.Containers) 434 return nil 435 }) 436 // expect a call to create the Service 437 cli.EXPECT(). 438 Patch(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). 439 DoAndReturn(func(ctx context.Context, service *corev1.Service, patch client.Patch, applyOpts ...client.PatchOption) error { 440 assert.Equal(serviceAPIVersion, service.APIVersion) 441 assert.Equal(serviceKind, service.Kind) 442 assert.Equal(deploymentTemplate.Metadata.Name, service.GetName()) 443 assert.Equal(deploymentTemplate.Metadata.Namespace, service.GetNamespace()) 444 assert.Contains(service.GetLabels(), labelKey) 445 assert.Contains(service.GetLabels(), "test-key") 446 assert.Contains(service.GetAnnotations(), "test-key") 447 assert.Contains(service.Spec.Selector, labelKey) 448 assert.Equal(service.Spec.Ports[0], testServicePort) 449 assert.Equal(service.Spec.Type, corev1.ServiceTypeClusterIP) 450 return nil 451 }) 452 // expect a call to status update 453 cli.EXPECT().Status().Return(mockStatus).AnyTimes() 454 mockStatus.EXPECT(). 455 Update(gomock.Any(), gomock.Any(), gomock.Any()). 456 DoAndReturn(func(ctx context.Context, workload *vzapi.VerrazzanoHelidonWorkload, opts ...client.UpdateOption) error { 457 assert.Len(workload.Status.Resources, 2) 458 return nil 459 }) 460 461 // create a request and reconcile it 462 request := newRequest(namespace, "unit-test-verrazzano-helidon-workload") 463 reconciler := newReconciler(cli) 464 result, err := reconciler.Reconcile(context.TODO(), request) 465 466 mocker.Finish() 467 assert.NoError(err) 468 assert.Equal(false, result.Requeue) 469 } 470 471 // TestReconcileCreateHelidonWithMultipleContainers tests the basic happy path of reconciling a VerrazzanoHelidonWorkload with multiple containers. 472 // We expect to write out a Deployment and Service but we aren't adding logging or any other scopes or traits. 473 // GIVEN a VerrazzanoHelidonWorkload resource is created 474 // AND that the workload has multiple containers 475 // WHEN the controller Reconcile function is called 476 // THEN expect a Deployment and Service to be written with multiple containers 477 func TestReconcileCreateHelidonWithMultipleContainers(t *testing.T) { 478 479 assert := asserts.New(t) 480 var mocker = gomock.NewController(t) 481 var cli = mocks.NewMockClient(mocker) 482 mockStatus := mocks.NewMockStatusWriter(mocker) 483 484 appConfigName := "unit-test-app-config" 485 componentName := "unit-test-component" 486 labels := map[string]string{oam.LabelAppComponent: componentName, oam.LabelAppName: appConfigName} 487 helidonTestContainerPort := corev1.ContainerPort{ 488 ContainerPort: 8080, 489 Name: "http", 490 } 491 helidonTestContainerPort2 := corev1.ContainerPort{ 492 ContainerPort: 8081, 493 Name: "udp", 494 Protocol: corev1.ProtocolUDP, 495 } 496 helidonTestContainer := corev1.Container{ 497 Name: "hello-helidon-container-new", 498 Image: "ghcr.io/verrazzano/example-helidon-greet-app-v1:1.0.0-1-20211215184123-0a1b633", 499 Ports: []corev1.ContainerPort{ 500 helidonTestContainerPort, 501 }, 502 } 503 helidonTestContainer2 := corev1.Container{ 504 Name: "hello-helidon-container-new2", 505 Image: "ghcr.io/verrazzano/example-helidon-greet-app-v1:1.0.0-1-20211215184123-0a1b633", 506 Ports: []corev1.ContainerPort{ 507 helidonTestContainerPort2, 508 }, 509 } 510 deploymentTemplate := &vzapi.DeploymentTemplate{ 511 Metadata: metav1.ObjectMeta{ 512 Name: "hello-helidon-deployment-new", 513 Namespace: namespace, 514 Labels: map[string]string{ 515 "app": "hello-helidon-deploy-new", 516 }, 517 }, 518 PodSpec: corev1.PodSpec{ 519 Containers: []corev1.Container{ 520 helidonTestContainer, 521 helidonTestContainer2, 522 }, 523 }, 524 } 525 526 // expect call to fetch existing deployment 527 cli.EXPECT(). 528 Get(gomock.Any(), types.NamespacedName{Namespace: namespace, Name: "hello-helidon-deployment-new"}, gomock.Not(gomock.Nil()), gomock.Any()). 529 DoAndReturn(func(ctx context.Context, name types.NamespacedName, deployment *appsv1.Deployment, opt ...client.GetOption) error { 530 return k8serrors.NewNotFound(k8sschema.GroupResource{}, "test") 531 }) 532 // expect a call to fetch the application configuration 533 cli.EXPECT(). 534 Get(gomock.Any(), types.NamespacedName{Namespace: namespace, Name: "unit-test-app-config"}, gomock.Not(gomock.Nil()), gomock.Any()). 535 DoAndReturn(func(ctx context.Context, name types.NamespacedName, appconf *oamapi.ApplicationConfiguration, opt ...client.GetOption) error { 536 appconf.Namespace = name.Namespace 537 appconf.Name = name.Name 538 appconf.APIVersion = oamapi.SchemeGroupVersion.String() 539 appconf.Kind = oamapi.ApplicationConfigurationKind 540 appconf.Spec.Components = []oamapi.ApplicationConfigurationComponent{{ComponentName: "unit-test-component"}} 541 return nil 542 }) 543 // expect a call to fetch the VerrazzanoHelidonWorkload 544 cli.EXPECT(). 545 Get(gomock.Any(), types.NamespacedName{Namespace: namespace, Name: "unit-test-verrazzano-helidon-workload"}, gomock.Not(gomock.Nil()), gomock.Any()). 546 DoAndReturn(func(ctx context.Context, name types.NamespacedName, workload *vzapi.VerrazzanoHelidonWorkload, opt ...client.GetOption) error { 547 workload.Spec.DeploymentTemplate = *deploymentTemplate 548 workload.ObjectMeta.Labels = labels 549 workload.APIVersion = vzapi.SchemeGroupVersion.String() 550 workload.Kind = "VerrazzanoHelidonWorkload" 551 workload.Namespace = namespace 552 return nil 553 }) 554 // expect a call to create the Deployment 555 cli.EXPECT(). 556 Patch(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). 557 DoAndReturn(func(ctx context.Context, deploy *appsv1.Deployment, patch client.Patch, applyOpts ...client.PatchOption) error { 558 assert.Equal(deploymentAPIVersion, deploy.APIVersion) 559 assert.Equal(deploymentKind, deploy.Kind) 560 // make sure the OAM component and app name labels were copied 561 assert.Equal(map[string]string{"app": "hello-helidon-deploy-new", "app.oam.dev/component": "unit-test-component", "app.oam.dev/name": "unit-test-app-config"}, deploy.GetLabels()) 562 assert.Equal([]corev1.Container{ 563 helidonTestContainer, 564 helidonTestContainer2, 565 }, deploy.Spec.Template.Spec.Containers) 566 567 return nil 568 }) 569 // expect a call to create the Service 570 cli.EXPECT(). 571 Patch(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). 572 DoAndReturn(func(ctx context.Context, service *corev1.Service, patch client.Patch, applyOpts ...client.PatchOption) error { 573 assert.Equal(serviceAPIVersion, service.APIVersion) 574 assert.Equal(serviceKind, service.Kind) 575 assert.Equal(service.Spec.Ports[0].Name, helidonTestContainer.Name+"-"+strconv.FormatInt(int64(helidonTestContainer.Ports[0].ContainerPort), 10)) 576 assert.Equal(service.Spec.Ports[0].Port, helidonTestContainer.Ports[0].ContainerPort) 577 assert.Equal(service.Spec.Ports[0].TargetPort, intstr.FromInt(int(helidonTestContainer.Ports[0].ContainerPort))) 578 assert.Equal(service.Spec.Ports[0].Protocol, corev1.ProtocolTCP) 579 assert.Equal(service.Spec.Ports[1].Name, helidonTestContainer2.Name+"-"+strconv.FormatInt(int64(helidonTestContainer2.Ports[0].ContainerPort), 10)) 580 assert.Equal(service.Spec.Ports[1].Port, helidonTestContainer2.Ports[0].ContainerPort) 581 assert.Equal(service.Spec.Ports[1].TargetPort, intstr.FromInt(int(helidonTestContainer2.Ports[0].ContainerPort))) 582 assert.Equal(service.Spec.Ports[1].Protocol, helidonTestContainer2.Ports[0].Protocol) 583 return nil 584 }) 585 // expect a call to status update 586 cli.EXPECT().Status().Return(mockStatus).AnyTimes() 587 mockStatus.EXPECT(). 588 Update(gomock.Any(), gomock.Any(), gomock.Any()). 589 DoAndReturn(func(ctx context.Context, workload *vzapi.VerrazzanoHelidonWorkload, opts ...client.UpdateOption) error { 590 assert.Len(workload.Status.Resources, 2) 591 return nil 592 }) 593 594 // create a request and reconcile it 595 request := newRequest(namespace, "unit-test-verrazzano-helidon-workload") 596 reconciler := newReconciler(cli) 597 result, err := reconciler.Reconcile(context.TODO(), request) 598 599 mocker.Finish() 600 assert.NoError(err) 601 assert.Equal(false, result.Requeue) 602 } 603 604 // TestReconcileCreateVerrazzanoHelidonWorkloadWithLoggingScope tests the basic happy path of reconciling a VerrazzanoHelidonWorkload that has a logging scope. 605 // We expect to write out a Deployment, Service and Configmap. 606 // GIVEN a VerrazzanoHelidonWorkload resource is created 607 // AND that the workload has a logging scope applied 608 // WHEN the controller Reconcile function is called 609 // THEN expect a Deployment, Service and Configmap to be written 610 func TestReconcileCreateVerrazzanoHelidonWorkloadWithLoggingScope(t *testing.T) { 611 612 assert := asserts.New(t) 613 var mocker = gomock.NewController(t) 614 var cli = mocks.NewMockClient(mocker) 615 mockStatus := mocks.NewMockStatusWriter(mocker) 616 617 testNamespace := "test-namespace" 618 loggingSecretName := "test-secret-name" 619 620 fluentdImage := "unit-test-image:latest" 621 // set the Fluentd image which is obtained via env then reset at end of test 622 initialDefaultFluentdImage := logging.DefaultFluentdImage 623 logging.DefaultFluentdImage = fluentdImage 624 defer func() { logging.DefaultFluentdImage = initialDefaultFluentdImage }() 625 626 params := map[string]string{ 627 "##APPCONF_NAME##": "test-appconf", 628 "##APPCONF_NAMESPACE##": testNamespace, 629 "##COMPONENT_NAME##": "test-component", 630 "##SCOPE_NAME##": "test-scope", 631 "##SCOPE_NAMESPACE##": testNamespace, 632 "##INGEST_URL##": "http://test-ingest-host:9200", 633 "##INGEST_SECRET_NAME##": loggingSecretName, 634 "##FLUENTD_IMAGE##": "test-fluentd-image-name", 635 "##WORKLOAD_APIVER##": "oam.verrazzano.io/v1alpha1", 636 "##WORKLOAD_KIND##": "VerrazzanoHelidonWorkload", 637 "##WORKLOAD_NAME##": "test-workload-name", 638 "##WORKLOAD_NAMESPACE##": testNamespace, 639 "##DEPLOYMENT_NAME##": "test-deployment", 640 "##CONTAINER_NAME##": "test-container", 641 "##CONTAINER_IMAGE##": "test-container-image", 642 "##CONTAINER_PORT_NAME##": "http", 643 "##CONTAINER_PORT_NUMBER##": "8080", 644 "##LOGGING_SCOPE_NAME##": "test-logging-scope", 645 "##INGRESS_TRAIT_NAME##": "test-ingress-trait", 646 "##INGRESS_TRAIT_PATH##": "/test-ingress-path", 647 } 648 // expect call to fetch existing deployment 649 cli.EXPECT(). 650 Get(gomock.Any(), types.NamespacedName{Namespace: testNamespace, Name: "test-deployment"}, gomock.Not(gomock.Nil()), gomock.Any()). 651 DoAndReturn(func(ctx context.Context, name types.NamespacedName, deployment *appsv1.Deployment, opt ...client.GetOption) error { 652 return k8serrors.NewNotFound(k8sschema.GroupResource{}, "test") 653 }) 654 // expect a call to fetch the application configuration 655 cli.EXPECT(). 656 Get(gomock.Any(), types.NamespacedName{Namespace: testNamespace, Name: "test-appconf"}, gomock.Not(gomock.Nil()), gomock.Any()). 657 DoAndReturn(func(ctx context.Context, name types.NamespacedName, appconf *oamapi.ApplicationConfiguration, opt ...client.GetOption) error { 658 assert.NoError(updateObjectFromYAMLTemplate(appconf, "testdata/templates/helidon_appconf_with_ingress_and_logging.yaml", params)) 659 return nil 660 }).Times(1) 661 // expect a call to fetch the VerrazzanoHelidonWorkload 662 cli.EXPECT(). 663 Get(gomock.Any(), types.NamespacedName{Namespace: testNamespace, Name: "test-verrazzano-helidon-workload"}, gomock.Not(gomock.Nil()), gomock.Any()). 664 DoAndReturn(func(ctx context.Context, name types.NamespacedName, workload *vzapi.VerrazzanoHelidonWorkload, opt ...client.GetOption) error { 665 assert.NoError(updateObjectFromYAMLTemplate(workload, helidonWorkload, params)) 666 return nil 667 }).Times(1) 668 669 // expect a call to create the Deployment 670 cli.EXPECT(). 671 Patch(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). 672 DoAndReturn(func(ctx context.Context, deploy *appsv1.Deployment, patch client.Patch, applyOpts ...client.PatchOption) error { 673 assert.Equal(deploymentAPIVersion, deploy.APIVersion) 674 assert.Equal(deploymentKind, deploy.Kind) 675 // make sure the OAM component and app name labels were copied 676 assert.Equal(map[string]string{"app.oam.dev/component": "test-component", "app.oam.dev/name": "test-appconf"}, deploy.GetLabels()) 677 assert.Equal(params["##CONTAINER_NAME##"], deploy.Spec.Template.Spec.Containers[0].Name) 678 assert.Len(deploy.Spec.Template.Spec.Containers, 1, "Expect 4 containers: app+sidecar") 679 680 // The app container should be unmodified for the Helidon use case. 681 c, found := findContainer(deploy.Spec.Template.Spec.Containers, "test-container") 682 assert.True(found, "Expected to find app container test-container") 683 assert.Equal(c.Image, "test-container-image") 684 assert.Len(c.Ports, 1) 685 assert.Equal(c.Ports[0].Name, "http") 686 assert.Equal(c.Ports[0].ContainerPort, int32(8080)) 687 assert.Nil(c.VolumeMounts, "Expected app container to have no volume mounts") 688 return nil 689 }) 690 // expect a call to create the Service 691 cli.EXPECT(). 692 Patch(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). 693 DoAndReturn(func(ctx context.Context, service *corev1.Service, patch client.Patch, applyOpts ...client.PatchOption) error { 694 assert.Equal(serviceAPIVersion, service.APIVersion) 695 assert.Equal(serviceKind, service.Kind) 696 return nil 697 }) 698 // expect a call to status update 699 cli.EXPECT().Status().Return(mockStatus).AnyTimes() 700 mockStatus.EXPECT(). 701 Update(gomock.Any(), gomock.Any(), gomock.Any()). 702 DoAndReturn(func(ctx context.Context, workload *vzapi.VerrazzanoHelidonWorkload, opts ...client.UpdateOption) error { 703 assert.Len(workload.Status.Resources, 2) 704 return nil 705 }) 706 707 // create a request and reconcile it 708 request := newRequest(testNamespace, "test-verrazzano-helidon-workload") 709 reconciler := newReconciler(cli) 710 result, err := reconciler.Reconcile(context.TODO(), request) 711 712 mocker.Finish() 713 assert.NoError(err) 714 assert.Equal(false, result.Requeue) 715 } 716 717 // TestReconcileCreateVerrazzanoHelidonWorkloadWithMultipleContainersAndLoggingScope tests correct sidecar setup for a workload with multiple containers. 718 // GIVEN a VerrazzanoHelidonWorkload resource is created 719 // AND that the workload has multiple containers 720 // AND that the workload has a logging scope applied 721 // WHEN the controller Reconcile function is called 722 // THEN expect a Deployment, Service and Configmap to be written 723 // AND expect that each application container has an associated logging sidecar container 724 func TestReconcileCreateVerrazzanoHelidonWorkloadWithMultipleContainersAndLoggingScope(t *testing.T) { 725 726 assert := asserts.New(t) 727 var mocker = gomock.NewController(t) 728 var cli = mocks.NewMockClient(mocker) 729 mockStatus := mocks.NewMockStatusWriter(mocker) 730 731 testNamespace := "test-namespace" 732 loggingSecretName := "test-secret-name" 733 734 fluentdImage := "unit-test-image:latest" 735 // set the Fluentd image which is obtained via env then reset at end of test 736 initialDefaultFluentdImage := logging.DefaultFluentdImage 737 logging.DefaultFluentdImage = fluentdImage 738 defer func() { logging.DefaultFluentdImage = initialDefaultFluentdImage }() 739 740 params := map[string]string{ 741 "##APPCONF_NAME##": "test-appconf", 742 "##APPCONF_NAMESPACE##": testNamespace, 743 "##COMPONENT_NAME##": "test-component", 744 "##SCOPE_NAME##": "test-scope", 745 "##SCOPE_NAMESPACE##": testNamespace, 746 "##INGEST_URL##": "http://test-ingest-host:9200", 747 "##INGEST_SECRET_NAME##": loggingSecretName, 748 "##FLUENTD_IMAGE##": "test-fluentd-image-name", 749 "##WORKLOAD_APIVER##": "oam.verrazzano.io/v1alpha1", 750 "##WORKLOAD_KIND##": "VerrazzanoHelidonWorkload", 751 "##WORKLOAD_NAME##": "test-workload-name", 752 "##WORKLOAD_NAMESPACE##": testNamespace, 753 "##DEPLOYMENT_NAME##": "test-deployment", 754 "##CONTAINER_NAME_1##": "test-container-1", 755 "##CONTAINER_IMAGE_1##": "test-container-image-1", 756 "##CONTAINER_PORT_NAME_1##": "http1", 757 "##CONTAINER_PORT_NUMBER_1##": "8081", 758 "##CONTAINER_NAME_2##": "test-container-2", 759 "##CONTAINER_IMAGE_2##": "test-container-image-2", 760 "##CONTAINER_PORT_NAME_2##": "http2", 761 "##CONTAINER_PORT_NUMBER_2##": "8082", 762 "##LOGGING_SCOPE_NAME##": "test-logging-scope", 763 "##INGRESS_TRAIT_NAME##": "test-ingress-trait", 764 "##INGRESS_TRAIT_PATH##": "/test-ingress-path", 765 } 766 // expect call to fetch existing deployment 767 cli.EXPECT(). 768 Get(gomock.Any(), types.NamespacedName{Namespace: testNamespace, Name: "test-deployment"}, gomock.Not(gomock.Nil()), gomock.Any()). 769 DoAndReturn(func(ctx context.Context, name types.NamespacedName, deployment *appsv1.Deployment, opt ...client.GetOption) error { 770 return k8serrors.NewNotFound(k8sschema.GroupResource{}, "test") 771 }) 772 // expect a call to fetch the application configuration 773 cli.EXPECT(). 774 Get(gomock.Any(), types.NamespacedName{Namespace: testNamespace, Name: "test-appconf"}, gomock.Not(gomock.Nil()), gomock.Any()). 775 DoAndReturn(func(ctx context.Context, name types.NamespacedName, appconf *oamapi.ApplicationConfiguration, opt ...client.GetOption) error { 776 assert.NoError(updateObjectFromYAMLTemplate(appconf, "testdata/templates/helidon_appconf_with_ingress_and_logging.yaml", params)) 777 return nil 778 }) 779 // expect a call to fetch the VerrazzanoHelidonWorkload 780 cli.EXPECT(). 781 Get(gomock.Any(), types.NamespacedName{Namespace: testNamespace, Name: "test-verrazzano-helidon-workload"}, gomock.Not(gomock.Nil()), gomock.Any()). 782 DoAndReturn(func(ctx context.Context, name types.NamespacedName, workload *vzapi.VerrazzanoHelidonWorkload, opt ...client.GetOption) error { 783 assert.NoError(updateObjectFromYAMLTemplate(workload, "testdata/templates/helidon_workload_multi_container.yaml", params)) 784 return nil 785 }).Times(1) 786 787 // expect a call to create the Deployment 788 cli.EXPECT(). 789 Patch(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). 790 DoAndReturn(func(ctx context.Context, deploy *appsv1.Deployment, patch client.Patch, applyOpts ...client.PatchOption) error { 791 assert.Equal(deploymentAPIVersion, deploy.APIVersion) 792 assert.Equal(deploymentKind, deploy.Kind) 793 // make sure the OAM component and app name labels were copied 794 assert.Equal(map[string]string{"app.oam.dev/component": "test-component", "app.oam.dev/name": "test-appconf"}, deploy.GetLabels()) 795 assert.Equal(params["##CONTAINER_NAME_1##"], deploy.Spec.Template.Spec.Containers[0].Name) 796 797 // There should be 4 containers because a sidecar will be added for each original container. 798 assert.Len(deploy.Spec.Template.Spec.Containers, 2, "Expect 2 containers.") 799 800 // The first app container should be unmodified. 801 c, found := findContainer(deploy.Spec.Template.Spec.Containers, "test-container-1") 802 assert.True(found, "Expected to find app container test-container") 803 assert.Equal(c.Image, "test-container-image-1") 804 assert.Len(c.Ports, 1) 805 assert.Equal(c.Ports[0].Name, "http1") 806 assert.Equal(c.Ports[0].ContainerPort, int32(8081)) 807 assert.Nil(c.VolumeMounts, "Expected app container to have no volume mounts") 808 809 // The second app container should be unmodified. 810 c, found = findContainer(deploy.Spec.Template.Spec.Containers, "test-container-2") 811 assert.True(found, "Expected to find app container test-container") 812 assert.Equal(c.Image, "test-container-image-2") 813 assert.Len(c.Ports, 1) 814 assert.Equal(c.Ports[0].Name, "http2") 815 assert.Equal(c.Ports[0].ContainerPort, int32(8082)) 816 assert.Nil(c.VolumeMounts, "Expected app container to have no volume mounts") 817 return nil 818 }) 819 // expect a call to create the Service 820 cli.EXPECT(). 821 Patch(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). 822 DoAndReturn(func(ctx context.Context, service *corev1.Service, patch client.Patch, applyOpts ...client.PatchOption) error { 823 assert.Equal(serviceAPIVersion, service.APIVersion) 824 assert.Equal(serviceKind, service.Kind) 825 return nil 826 }) 827 // expect a call to status update 828 cli.EXPECT().Status().Return(mockStatus).AnyTimes() 829 mockStatus.EXPECT(). 830 Update(gomock.Any(), gomock.Any(), gomock.Any()). 831 DoAndReturn(func(ctx context.Context, workload *vzapi.VerrazzanoHelidonWorkload, opts ...client.UpdateOption) error { 832 assert.Len(workload.Status.Resources, 2) 833 return nil 834 }) 835 836 // create a request and reconcile it 837 request := newRequest(testNamespace, "test-verrazzano-helidon-workload") 838 reconciler := newReconciler(cli) 839 result, err := reconciler.Reconcile(context.TODO(), request) 840 841 mocker.Finish() 842 assert.NoError(err) 843 assert.Equal(false, result.Requeue) 844 } 845 846 // newScheme creates a new scheme that includes this package's object to use for testing 847 func newScheme() *runtime.Scheme { 848 scheme := runtime.NewScheme() 849 _ = vzapi.AddToScheme(scheme) 850 return scheme 851 } 852 853 // newReconciler creates a new reconciler for testing 854 // c - The K8s client to inject into the reconciler 855 func newReconciler(c client.Client) Reconciler { 856 scheme := newScheme() 857 metricsReconciler := &metricstrait.Reconciler{Client: c, Scheme: scheme, Scraper: "verrazzano-system/vmi-system-prometheus-0"} 858 return Reconciler{ 859 Client: c, 860 Log: zap.S().With("test"), 861 Scheme: scheme, 862 Metrics: metricsReconciler, 863 } 864 } 865 866 // newRequest creates a new reconciler request for testing 867 // namespace - The namespace to use in the request 868 // name - The name to use in the request 869 func newRequest(namespace string, name string) ctrl.Request { 870 return ctrl.Request{ 871 NamespacedName: types.NamespacedName{ 872 Namespace: namespace, 873 Name: name, 874 }, 875 } 876 } 877 878 // readTemplate reads a string template from a file and replaces values in the template from param maps 879 // template - The filename of a template 880 // params - a vararg of param maps 881 func readTemplate(template string, params ...map[string]string) (string, error) { 882 bytes, err := os.ReadFile("../../" + template) 883 if err != nil { 884 bytes, err = os.ReadFile("../" + template) 885 if err != nil { 886 bytes, err = os.ReadFile(template) 887 if err != nil { 888 return "", err 889 } 890 } 891 } 892 content := string(bytes) 893 for _, p := range params { 894 for k, v := range p { 895 content = strings.ReplaceAll(content, k, v) 896 } 897 } 898 return content, nil 899 } 900 901 // updateUnstructuredFromYAMLTemplate updates an unstructured from a populated YAML template file. 902 // uns - The unstructured to update 903 // template - The template file 904 // params - The param maps to merge into the template 905 func updateUnstructuredFromYAMLTemplate(uns *unstructured.Unstructured, template string, params ...map[string]string) error { 906 str, err := readTemplate(template, params...) 907 if err != nil { 908 return err 909 } 910 bytes, err := yaml.YAMLToJSON([]byte(str)) 911 if err != nil { 912 return err 913 } 914 _, _, err = unstructured.UnstructuredJSONScheme.Decode(bytes, nil, uns) 915 if err != nil { 916 return err 917 } 918 return nil 919 } 920 921 // updateObjectFromYAMLTemplate updates an object from a populated YAML template file. 922 // uns - The unstructured to update 923 // template - The template file 924 // params - The param maps to merge into the template 925 func updateObjectFromYAMLTemplate(obj interface{}, template string, params ...map[string]string) error { 926 uns := unstructured.Unstructured{} 927 err := updateUnstructuredFromYAMLTemplate(&uns, template, params...) 928 if err != nil { 929 return err 930 } 931 err = runtime.DefaultUnstructuredConverter.FromUnstructured(uns.Object, obj) 932 if err != nil { 933 return err 934 } 935 return nil 936 } 937 938 // findContainer finds a container in a slice by name. 939 func findContainer(containers []corev1.Container, name string) (*corev1.Container, bool) { 940 for i, c := range containers { 941 if c.Name == name { 942 return &containers[i], true 943 } 944 } 945 return nil, false 946 } 947 948 func getTestDeployment(restartVersion string) *appsv1.Deployment { 949 deployment := &appsv1.Deployment{} 950 annotateRestartVersion(deployment, restartVersion) 951 return deployment 952 } 953 954 func annotateRestartVersion(deployment *appsv1.Deployment, restartVersion string) { 955 deployment.Spec.Template.ObjectMeta.Annotations = make(map[string]string) 956 deployment.Spec.Template.ObjectMeta.Annotations[vzconst.RestartVersionAnnotation] = restartVersion 957 } 958 959 // TestReconcileRestart tests reconciling a VerrazzanoHelidonWorkload when the restart-version specified in the annotations. 960 // This should result in restart-version written to the Helidon Deployment. 961 // GIVEN a VerrazzanoHelidonWorkload resource 962 // WHEN the controller Reconcile function is called and the restart-version is specified 963 // THEN the restart-version written 964 func TestReconcileRestart(t *testing.T) { 965 966 assert := asserts.New(t) 967 var mocker = gomock.NewController(t) 968 var cli = mocks.NewMockClient(mocker) 969 mockStatus := mocks.NewMockStatusWriter(mocker) 970 971 testNamespace := "test-namespace" 972 loggingSecretName := "test-secret-name" 973 974 appConfigName := "test-appconf" 975 componentName := "test-component" 976 labels := map[string]string{oam.LabelAppComponent: componentName, oam.LabelAppName: appConfigName} 977 annotations := map[string]string{vzconst.RestartVersionAnnotation: testRestartVersion} 978 979 fluentdImage := "unit-test-image:latest" 980 // set the Fluentd image which is obtained via env then reset at end of test 981 initialDefaultFluentdImage := logging.DefaultFluentdImage 982 logging.DefaultFluentdImage = fluentdImage 983 defer func() { logging.DefaultFluentdImage = initialDefaultFluentdImage }() 984 985 params := map[string]string{ 986 "##APPCONF_NAME##": appConfigName, 987 "##APPCONF_NAMESPACE##": testNamespace, 988 "##COMPONENT_NAME##": componentName, 989 "##SCOPE_NAME##": "test-scope", 990 "##SCOPE_NAMESPACE##": testNamespace, 991 "##INGEST_URL##": "http://test-ingest-host:9200", 992 "##INGEST_SECRET_NAME##": loggingSecretName, 993 "##FLUENTD_IMAGE##": "test-fluentd-image-name", 994 "##WORKLOAD_APIVER##": "oam.verrazzano.io/v1alpha1", 995 "##WORKLOAD_KIND##": "VerrazzanoHelidonWorkload", 996 "##WORKLOAD_NAME##": "test-workload-name", 997 "##WORKLOAD_NAMESPACE##": testNamespace, 998 "##DEPLOYMENT_NAME##": "test-deployment", 999 "##CONTAINER_NAME##": "test-container", 1000 "##CONTAINER_IMAGE##": "test-container-image", 1001 "##CONTAINER_PORT_NAME##": "http", 1002 "##CONTAINER_PORT_NUMBER##": "8080", 1003 "##LOGGING_SCOPE_NAME##": "test-logging-scope", 1004 "##INGRESS_TRAIT_NAME##": "test-ingress-trait", 1005 "##INGRESS_TRAIT_PATH##": "/test-ingress-path", 1006 } 1007 // expect call to fetch existing deployment 1008 cli.EXPECT(). 1009 Get(gomock.Any(), types.NamespacedName{Namespace: testNamespace, Name: "test-deployment"}, gomock.Not(gomock.Nil()), gomock.Any()). 1010 DoAndReturn(func(ctx context.Context, name types.NamespacedName, deployment *appsv1.Deployment, opt ...client.GetOption) error { 1011 return nil 1012 }) 1013 // expect a call to fetch the application configuration 1014 cli.EXPECT(). 1015 Get(gomock.Any(), types.NamespacedName{Namespace: testNamespace, Name: "test-appconf"}, gomock.Not(gomock.Nil()), gomock.Any()). 1016 DoAndReturn(func(ctx context.Context, name types.NamespacedName, appconf *oamapi.ApplicationConfiguration, opt ...client.GetOption) error { 1017 assert.NoError(updateObjectFromYAMLTemplate(appconf, "testdata/templates/helidon_appconf_with_ingress_and_logging.yaml", params)) 1018 return nil 1019 }).Times(1) 1020 // expect a call to fetch the VerrazzanoHelidonWorkload 1021 cli.EXPECT(). 1022 Get(gomock.Any(), types.NamespacedName{Namespace: testNamespace, Name: "test-verrazzano-helidon-workload"}, gomock.Not(gomock.Nil()), gomock.Any()). 1023 DoAndReturn(func(ctx context.Context, name types.NamespacedName, workload *vzapi.VerrazzanoHelidonWorkload, opt ...client.GetOption) error { 1024 assert.NoError(updateObjectFromYAMLTemplate(workload, helidonWorkload, params)) 1025 workload.ObjectMeta.Labels = labels 1026 workload.ObjectMeta.Annotations = annotations 1027 return nil 1028 }).Times(1) 1029 1030 // expect a call to create the Deployment 1031 cli.EXPECT(). 1032 Patch(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). 1033 DoAndReturn(func(ctx context.Context, deploy *appsv1.Deployment, patch client.Patch, applyOpts ...client.PatchOption) error { 1034 assert.Equal(deploymentAPIVersion, deploy.APIVersion) 1035 assert.Equal(deploymentKind, deploy.Kind) 1036 // make sure the OAM component and app name labels were copied 1037 assert.Equal(labels, deploy.GetLabels()) 1038 assert.Equal(params["##CONTAINER_NAME##"], deploy.Spec.Template.Spec.Containers[0].Name) 1039 assert.Len(deploy.Spec.Template.Spec.Containers, 1) 1040 1041 // The app container should be unmodified for the Helidon use case. 1042 c, found := findContainer(deploy.Spec.Template.Spec.Containers, "test-container") 1043 assert.True(found, "Expected to find app container test-container") 1044 assert.Equal(c.Image, "test-container-image") 1045 assert.Len(c.Ports, 1) 1046 assert.Equal(c.Ports[0].Name, "http") 1047 assert.Equal(c.Ports[0].ContainerPort, int32(8080)) 1048 assert.Nil(c.VolumeMounts, "Expected app container to have no volume mounts") 1049 return nil 1050 }) 1051 // expect a call to create the Service 1052 cli.EXPECT(). 1053 Patch(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). 1054 DoAndReturn(func(ctx context.Context, service *corev1.Service, patch client.Patch, applyOpts ...client.PatchOption) error { 1055 assert.Equal(serviceAPIVersion, service.APIVersion) 1056 assert.Equal(serviceKind, service.Kind) 1057 return nil 1058 }) 1059 // expect a call to list the deployment 1060 cli.EXPECT(). 1061 List(gomock.Any(), gomock.Any(), gomock.Any()). 1062 DoAndReturn(func(ctx context.Context, list *appsv1.DeploymentList, opts ...client.ListOption) error { 1063 list.Items = []appsv1.Deployment{*getTestDeployment("")} 1064 return nil 1065 }) 1066 // expect a call to fetch the deployment 1067 cli.EXPECT(). 1068 Get(gomock.Any(), gomock.Any(), gomock.Not(gomock.Nil()), gomock.Any()). 1069 DoAndReturn(func(ctx context.Context, name types.NamespacedName, deployment *appsv1.Deployment, opt ...client.GetOption) error { 1070 annotateRestartVersion(deployment, "") 1071 return nil 1072 }) 1073 // expect a call to update the deployment 1074 cli.EXPECT(). 1075 Update(gomock.Any(), gomock.AssignableToTypeOf(&appsv1.Deployment{}), gomock.Any()). 1076 DoAndReturn(func(ctx context.Context, deploy *appsv1.Deployment, opts ...client.UpdateOption) error { 1077 assert.Equal(testRestartVersion, deploy.Spec.Template.ObjectMeta.Annotations[vzconst.RestartVersionAnnotation]) 1078 return nil 1079 }) 1080 // expect a call to status update 1081 cli.EXPECT().Status().Return(mockStatus).AnyTimes() 1082 mockStatus.EXPECT(). 1083 Update(gomock.Any(), gomock.Any(), gomock.Any()). 1084 DoAndReturn(func(ctx context.Context, workload *vzapi.VerrazzanoHelidonWorkload, opts ...client.UpdateOption) error { 1085 assert.Len(workload.Status.Resources, 2) 1086 return nil 1087 }) 1088 1089 // create a request and reconcile it 1090 request := newRequest(testNamespace, "test-verrazzano-helidon-workload") 1091 reconciler := newReconciler(cli) 1092 result, err := reconciler.Reconcile(context.TODO(), request) 1093 1094 mocker.Finish() 1095 assert.NoError(err) 1096 assert.Equal(false, result.Requeue) 1097 } 1098 1099 // TestReconcileKubeSystem tests to make sure we do not reconcile 1100 // Any resource that belong to the kube-system namespace 1101 func TestReconcileKubeSystem(t *testing.T) { 1102 1103 assert := asserts.New(t) 1104 1105 var mocker = gomock.NewController(t) 1106 var cli = mocks.NewMockClient(mocker) 1107 1108 // create a request and reconcile it 1109 request := newRequest(vzconst.KubeSystem, "unit-test-verrazzano-helidon-workload") 1110 reconciler := newReconciler(cli) 1111 result, err := reconciler.Reconcile(context.TODO(), request) 1112 1113 // Validate the results 1114 mocker.Finish() 1115 assert.Nil(err) 1116 assert.True(result.IsZero()) 1117 } 1118 1119 // TestReconcileFailed tests to make sure the failure metric is being exposed 1120 func TestReconcileFailed(t *testing.T) { 1121 testAppConfigName := "unit-test-app-config" 1122 testNamespace := "test-ns" 1123 1124 assert := asserts.New(t) 1125 clientBuilder := fake.NewClientBuilder().WithScheme(k8scheme.Scheme).Build() 1126 // Create a request and reconcile it 1127 reconciler := newReconciler(clientBuilder) 1128 request := newRequest(testNamespace, testAppConfigName) 1129 reconcileerrorCounterObject, err := metricsexporter.GetSimpleCounterMetric(metricsexporter.HelidonReconcileError) 1130 assert.NoError(err) 1131 // Expect a call to fetch the error 1132 reconcileFailedCounterBefore := testutil.ToFloat64(reconcileerrorCounterObject.Get()) 1133 reconcileerrorCounterObject.Get().Inc() 1134 reconciler.Reconcile(context.TODO(), request) 1135 reconcileFailedCounterAfter := testutil.ToFloat64(reconcileerrorCounterObject.Get()) 1136 assert.Equal(reconcileFailedCounterBefore, reconcileFailedCounterAfter-1) 1137 } 1138 1139 // TestAddMetrics tests to add metrics and make sure no error comes 1140 func TestAddMetrics(t *testing.T) { 1141 assert := asserts.New(t) 1142 var mocker = gomock.NewController(t) 1143 var cli = mocks.NewMockClient(mocker) 1144 appConfigName := "test-appconf" 1145 componentName := "test-component" 1146 testNamespace := "test-namespace" 1147 loggingSecretName := "test-secret-name" 1148 params := map[string]string{ 1149 "##APPCONF_NAME##": appConfigName, 1150 "##APPCONF_NAMESPACE##": testNamespace, 1151 "##COMPONENT_NAME##": componentName, 1152 "##SCOPE_NAME##": "test-scope", 1153 "##SCOPE_NAMESPACE##": testNamespace, 1154 "##INGEST_URL##": "http://test-ingest-host:9200", 1155 "##INGEST_SECRET_NAME##": loggingSecretName, 1156 "##FLUENTD_IMAGE##": "test-fluentd-image-name", 1157 "##WORKLOAD_APIVER##": "oam.verrazzano.io/v1alpha1", 1158 "##WORKLOAD_KIND##": "VerrazzanoHelidonWorkload", 1159 "##WORKLOAD_NAME##": "test-workload-name", 1160 "##WORKLOAD_NAMESPACE##": testNamespace, 1161 "##DEPLOYMENT_NAME##": "test-deployment", 1162 "##LOGGING_SCOPE_NAME##": "test-logging-scope", 1163 "##INGRESS_TRAIT_NAME##": "test-ingress-trait", 1164 "##INGRESS_TRAIT_PATH##": "/test-ingress-path", 1165 } 1166 1167 labels := map[string]string{oam.LabelAppComponent: componentName, oam.LabelAppName: componentName} 1168 annotations := map[string]string{vzconst.RestartVersionAnnotation: testRestartVersion} 1169 request := types.NamespacedName{ 1170 Namespace: testNamespace, 1171 Name: componentName, 1172 } 1173 reconciler := newReconciler(cli) 1174 // Fetch the workload 1175 var workload vzapi.VerrazzanoHelidonWorkload 1176 cli.EXPECT(). 1177 // GIVEN a default mocker client 1178 // WHEN we call Get from the addMetrics method 1179 // THEN the call feeds workload to get metrics traits 1180 Get(gomock.Any(), request, gomock.Not(gomock.Nil()), gomock.Any()). 1181 DoAndReturn(func(ctx context.Context, name types.NamespacedName, workload1 *vzapi.VerrazzanoHelidonWorkload, opt ...client.GetOption) error { 1182 assert.NoError(updateObjectFromYAMLTemplate(workload1, helidonWorkload, params)) 1183 workload1.ObjectMeta.Labels = labels 1184 workload1.ObjectMeta.Annotations = annotations 1185 workload1.ObjectMeta.Namespace = testNamespace 1186 workload1.ObjectMeta.OwnerReferences = []metav1.OwnerReference{{Name: testNamespace}} 1187 return nil 1188 }) 1189 1190 cli.EXPECT(). 1191 // GIVEN a default mocker client 1192 // WHEN we call Get from the ComponentFromWorkloadLabels method 1193 // THEN the call feeds workload and component to get metrics traits 1194 Get(gomock.Any(), request, gomock.Not(gomock.Nil()), gomock.Any()). 1195 DoAndReturn(func(ctx context.Context, name types.NamespacedName, workload1 *oamapi.ApplicationConfiguration, opt ...client.GetOption) error { 1196 assert.NoError(updateObjectFromYAMLTemplate(workload1, helidonWorkload, params)) 1197 workload1.ObjectMeta.Labels = labels 1198 workload1.ObjectMeta.Annotations = annotations 1199 workload1.ObjectMeta.Namespace = testNamespace 1200 workload1.ObjectMeta.OwnerReferences = []metav1.OwnerReference{{Name: testNamespace}} 1201 workload1.Spec.Components = []oamapi.ApplicationConfigurationComponent{{ComponentName: componentName, Traits: []oamapi.ComponentTrait{{Trait: runtime.RawExtension{Raw: []byte(`{"kind":"MetricsTrait"}`)}}}}} 1202 return nil 1203 }) 1204 1205 cli.EXPECT(). 1206 // GIVEN a default mocker client 1207 // WHEN we call List from the MetricsTraitFromWorkloadLabels method 1208 // THEN the call gets MetricsTrait object associated with the workload 1209 List(gomock.Any(), gomock.Not(gomock.Nil()), gomock.Any()). 1210 DoAndReturn(func(ctx context.Context, list *vzapi.MetricsTraitList, opts ...client.ListOption) error { 1211 list.Items = []vzapi.MetricsTrait{{ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, OwnerReferences: []metav1.OwnerReference{{Name: testNamespace}}}}} 1212 list.Items[0].Spec.WorkloadReference.Name = params["##WORKLOAD_NAME##"] 1213 return nil 1214 }) 1215 1216 err := reconciler.Get(context.TODO(), request, &workload) 1217 assert.NoError(err) 1218 1219 log, err := clusters.GetResourceLogger("verrazzanohelidonworkload", request, &workload) 1220 assert.NoError(err) 1221 // Unwrap the apps/DeploymentSpec and meta/ObjectMeta 1222 deploy, err := reconciler.convertWorkloadToDeployment(&workload, log) 1223 assert.NoError(err) 1224 assert.NoError(reconciler.addMetrics(context.TODO(), log, workload.Namespace, &workload, deploy)) 1225 mocker.Finish() 1226 }