github.com/verrazzano/verrazzano@v1.7.1/application-operator/controllers/containerizedworkload/containerizedworkload_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 containerizedworkload 5 6 import ( 7 "context" 8 "os" 9 "strings" 10 "testing" 11 12 commonv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" 13 "github.com/crossplane/crossplane-runtime/pkg/meta" 14 oamcore "github.com/crossplane/oam-kubernetes-runtime/apis/core" 15 oamv1 "github.com/crossplane/oam-kubernetes-runtime/apis/core/v1alpha2" 16 "github.com/crossplane/oam-kubernetes-runtime/pkg/oam" 17 "github.com/golang/mock/gomock" 18 asserts "github.com/stretchr/testify/assert" 19 "github.com/verrazzano/verrazzano/application-operator/mocks" 20 vzconst "github.com/verrazzano/verrazzano/pkg/constants" 21 "github.com/verrazzano/verrazzano/pkg/log/vzlog" 22 "go.uber.org/zap" 23 appsv1 "k8s.io/api/apps/v1" 24 corev1 "k8s.io/api/core/v1" 25 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 27 "k8s.io/apimachinery/pkg/runtime" 28 "k8s.io/apimachinery/pkg/types" 29 ctrl "sigs.k8s.io/controller-runtime" 30 "sigs.k8s.io/controller-runtime/pkg/client" 31 fakes "sigs.k8s.io/controller-runtime/pkg/client/fake" 32 "sigs.k8s.io/yaml" 33 ) 34 35 const ( 36 testRestartVersion = "new-restart" 37 testNamespace = "test-namespace" 38 39 appConfigName = "test-appconf" 40 componentName = "test-component" 41 ) 42 43 // newScheme creates a new scheme that includes this package's object to use for testing 44 func newScheme() *runtime.Scheme { 45 scheme := runtime.NewScheme() 46 _ = oamcore.AddToScheme(scheme) 47 return scheme 48 } 49 50 // newReconciler creates a new reconciler for testing 51 func newReconciler(c client.Client) Reconciler { 52 return Reconciler{ 53 Client: c, 54 Log: zap.S().With("test"), 55 Scheme: newScheme(), 56 } 57 } 58 59 // newRequest creates a new reconciler request for testing 60 func newRequest(namespace string, name string) ctrl.Request { 61 return ctrl.Request{ 62 NamespacedName: types.NamespacedName{ 63 Namespace: namespace, 64 Name: name, 65 }, 66 } 67 } 68 69 // TestReconcileRestart tests reconciling a ContainerizedWorkload when the restart-version specified in the annotations. 70 // This should result in restart-version written to the Deployment. 71 // GIVEN a ContainerizedWorkload resource 72 // WHEN the controller Reconcile function is called and the restart-version is specified 73 // THEN the restart-version written 74 func TestReconcileRestart(t *testing.T) { 75 assert := asserts.New(t) 76 var mocker = gomock.NewController(t) 77 var cli = mocks.NewMockClient(mocker) 78 79 labels := map[string]string{oam.LabelAppComponent: componentName, oam.LabelAppName: appConfigName} 80 annotations := map[string]string{vzconst.RestartVersionAnnotation: testRestartVersion} 81 82 // expect a call to fetch the ContainerizedWorkload 83 params := map[string]string{ 84 "##OAM_APP_NAME##": "test-oam-app-name", 85 "##OAM_COMP_NAME##": "test-oam-comp-name", 86 "##TRAIT_NAME##": "test-trait-name", 87 "##TRAIT_NAMESPACE##": "test-namespace", 88 "##WORKLOAD_APIVER##": "core.oam.dev/v1alpha2", 89 "##WORKLOAD_KIND##": "ContainerizedWorkload", 90 "##WORKLOAD_NAME##": "test-workload-name", 91 "##PROMETHEUS_NAME##": "vmi-system-prometheus-0", 92 "##PROMETHEUS_NAMESPACE##": "verrazzano-system", 93 "##DEPLOYMENT_NAMESPACE##": "test-namespace", 94 "##DEPLOYMENT_NAME##": "test-workload-name", 95 } 96 cli.EXPECT(). 97 Get(gomock.Any(), types.NamespacedName{Namespace: testNamespace, Name: "test-verrazzano-containerized-workload"}, gomock.Not(gomock.Nil()), gomock.Any()). 98 DoAndReturn(func(ctx context.Context, name types.NamespacedName, workload *oamv1.ContainerizedWorkload, opts ...client.GetOption) error { 99 assert.NoError(updateObjectFromYAMLTemplate(workload, "testdata/templates/containerized_workload_deployment.yaml", params)) 100 workload.ObjectMeta.Labels = labels 101 workload.ObjectMeta.Annotations = annotations 102 return nil 103 }).Times(1) 104 // expect a call to list the deployment 105 cli.EXPECT(). 106 List(gomock.Any(), gomock.Any(), gomock.Any()). 107 DoAndReturn(func(ctx context.Context, list *appsv1.DeploymentList, opts ...client.ListOption) error { 108 list.Items = []appsv1.Deployment{*getTestDeployment("")} 109 return nil 110 }) 111 // expect a call to fetch the deployment 112 cli.EXPECT(). 113 Get(gomock.Any(), gomock.Any(), gomock.Not(gomock.Nil()), gomock.Any()). 114 DoAndReturn(func(ctx context.Context, name types.NamespacedName, deployment *appsv1.Deployment, opts ...client.GetOption) error { 115 annotateRestartVersion(deployment, "") 116 return nil 117 }) 118 // expect a call to update the deployment 119 cli.EXPECT(). 120 Update(gomock.Any(), gomock.AssignableToTypeOf(&appsv1.Deployment{}), gomock.Any()). 121 DoAndReturn(func(ctx context.Context, deploy *appsv1.Deployment, opts ...client.UpdateOption) error { 122 assert.Equal(testRestartVersion, deploy.Spec.Template.ObjectMeta.Annotations[vzconst.RestartVersionAnnotation]) 123 return nil 124 }) 125 126 // create a request and reconcile it 127 request := newRequest(testNamespace, "test-verrazzano-containerized-workload") 128 reconciler := newReconciler(cli) 129 result, err := reconciler.Reconcile(context.TODO(), request) 130 131 mocker.Finish() 132 assert.NoError(err) 133 assert.Equal(false, result.Requeue) 134 } 135 136 // TestReconcileKubeSystem tests to make sure we do not reconcile 137 // Any resource that belong to the kube-system namespace 138 func TestReconcileKubeSystem(t *testing.T) { 139 assert := asserts.New(t) 140 mocker := gomock.NewController(t) 141 cli := mocks.NewMockClient(mocker) 142 143 // create a request and reconcile it 144 request := newRequest(vzconst.KubeSystem, "test-verrazzano-containerized-workload") 145 reconciler := newReconciler(cli) 146 result, err := reconciler.Reconcile(context.TODO(), request) 147 148 // Validate the results 149 mocker.Finish() 150 assert.NoError(err) 151 assert.True(result.IsZero()) 152 } 153 154 // TestGetWorkloadService tests to make sure the workload service is being picked up 155 func TestGetWorkloadService(t *testing.T) { 156 a := asserts.New(t) 157 log := vzlog.DefaultLogger() 158 159 // GIVEN a ContainerizedWorkload resource 160 // WHEN the status is not populated with a service 161 // THEN no service is returned 162 emptyWorkload := oamv1.ContainerizedWorkload{} 163 cli := fakes.NewClientBuilder().Build() 164 reconciler := newReconciler(cli) 165 svc, err := reconciler.getWorkloadService(context.TODO(), emptyWorkload, log) 166 a.Nil(svc) 167 a.NoError(err) 168 169 // GIVEN a ContainerizedWorkload resource 170 // WHEN the status is populated with a service that doesn't exist 171 // THEN an error is returned 172 service := corev1.Service{ 173 TypeMeta: metav1.TypeMeta{ 174 Kind: "Service", 175 APIVersion: "v1", 176 }, 177 ObjectMeta: metav1.ObjectMeta{ 178 Name: "test-service", 179 UID: "test-uid", 180 Labels: map[string]string{}, 181 }, 182 } 183 statusWorkload := oamv1.ContainerizedWorkload{ 184 Status: oamv1.ContainerizedWorkloadStatus{ 185 Resources: []commonv1.TypedReference{*meta.TypedReferenceTo(&service, service.GroupVersionKind())}, 186 }, 187 } 188 cli = fakes.NewClientBuilder().Build() 189 reconciler = newReconciler(cli) 190 svc, err = reconciler.getWorkloadService(context.TODO(), statusWorkload, log) 191 a.Nil(svc) 192 a.Error(err) 193 194 // GIVEN a ContainerizedWorkload resource 195 // WHEN the status is populated with a service that exists 196 // THEN the service is returned 197 cli = fakes.NewClientBuilder().WithObjects(&service).Build() 198 reconciler = newReconciler(cli) 199 svc, err = reconciler.getWorkloadService(context.TODO(), statusWorkload, log) 200 a.Equal(svc.Name, service.Name) 201 a.Equal(svc.UID, service.UID) 202 a.NoError(err) 203 204 } 205 206 // TestUpdateServiceLabels tests the update to the service in the containerized workload status 207 func TestUpdateServiceLabels(t *testing.T) { 208 a := asserts.New(t) 209 210 // GIVEN a ContainerizedWorkload resource 211 // WHEN the status is empty 212 // THEN no error is returned 213 statusWorkload := oamv1.ContainerizedWorkload{} 214 cli := fakes.NewClientBuilder().Build() 215 reconciler := newReconciler(cli) 216 err := reconciler.updateServiceLabels(context.TODO(), statusWorkload, vzlog.DefaultLogger()) 217 a.NoError(err) 218 } 219 220 // updateObjectFromYAMLTemplate updates an object from a populated YAML template file. 221 // uns - The unstructured to update 222 // template - The template file 223 // params - The param maps to merge into the template 224 func updateObjectFromYAMLTemplate(obj interface{}, template string, params ...map[string]string) error { 225 uns := unstructured.Unstructured{} 226 err := updateUnstructuredFromYAMLTemplate(&uns, template, params...) 227 if err != nil { 228 return err 229 } 230 err = runtime.DefaultUnstructuredConverter.FromUnstructured(uns.Object, obj) 231 if err != nil { 232 return err 233 } 234 return nil 235 } 236 237 // updateUnstructuredFromYAMLTemplate updates an unstructured from a populated YAML template file. 238 // uns - The unstructured to update 239 // template - The template file 240 // params - The param maps to merge into the template 241 func updateUnstructuredFromYAMLTemplate(uns *unstructured.Unstructured, template string, params ...map[string]string) error { 242 str, err := readTemplate(template, params...) 243 if err != nil { 244 return err 245 } 246 bytes, err := yaml.YAMLToJSON([]byte(str)) 247 if err != nil { 248 return err 249 } 250 _, _, err = unstructured.UnstructuredJSONScheme.Decode(bytes, nil, uns) 251 if err != nil { 252 return err 253 } 254 return nil 255 } 256 257 // readTemplate reads a string template from a file and replaces values in the template from param maps 258 // template - The filename of a template 259 // params - a vararg of param maps 260 func readTemplate(template string, params ...map[string]string) (string, error) { 261 bytes, err := os.ReadFile("../../" + template) 262 if err != nil { 263 bytes, err = os.ReadFile("../" + template) 264 if err != nil { 265 bytes, err = os.ReadFile(template) 266 if err != nil { 267 return "", err 268 } 269 } 270 } 271 content := string(bytes) 272 for _, p := range params { 273 for k, v := range p { 274 content = strings.ReplaceAll(content, k, v) 275 } 276 } 277 return content, nil 278 } 279 280 func getTestDeployment(restartVersion string) *appsv1.Deployment { 281 deployment := &appsv1.Deployment{} 282 annotateRestartVersion(deployment, restartVersion) 283 return deployment 284 } 285 286 func annotateRestartVersion(deployment *appsv1.Deployment, restartVersion string) { 287 deployment.Spec.Template.ObjectMeta.Annotations = make(map[string]string) 288 deployment.Spec.Template.ObjectMeta.Annotations[vzconst.RestartVersionAnnotation] = restartVersion 289 }