github.com/verrazzano/verrazzano@v1.7.0/pkg/k8sutil/apply_yaml_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 k8sutil_test 5 6 import ( 7 "context" 8 "github.com/google/go-cmp/cmp" 9 "testing" 10 11 rbacv1 "k8s.io/api/rbac/v1" 12 13 "github.com/stretchr/testify/assert" 14 "github.com/verrazzano/verrazzano/pkg/k8sutil" 15 "github.com/verrazzano/verrazzano/tools/vz/pkg/constants" 16 "github.com/verrazzano/verrazzano/tools/vz/pkg/helpers" 17 appv1 "k8s.io/api/apps/v1" 18 corev1 "k8s.io/api/core/v1" 19 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 20 "k8s.io/apimachinery/pkg/types" 21 k8scheme "k8s.io/client-go/kubernetes/scheme" 22 "sigs.k8s.io/controller-runtime/pkg/client/fake" 23 ) 24 25 const ( 26 objects = "./testdata/objects" 27 testdata = "./testdata" 28 ) 29 30 func TestApplyD(t *testing.T) { 31 var tests = []struct { 32 name string 33 dir string 34 count int 35 isError bool 36 }{ 37 { 38 "should apply YAML files", 39 objects, 40 3, 41 false, 42 }, 43 { 44 "should fail to apply non-existent directories", 45 "blahblah", 46 0, 47 true, 48 }, 49 } 50 51 for _, tt := range tests { 52 t.Run(tt.name, func(t *testing.T) { 53 c := fake.NewClientBuilder().WithScheme(k8scheme.Scheme).Build() 54 y := k8sutil.NewYAMLApplier(c, "") 55 err := y.ApplyD(tt.dir) 56 if tt.isError { 57 assert.Error(t, err) 58 } else { 59 assert.NoError(t, err) 60 assert.Equal(t, tt.count, len(y.Objects())) 61 } 62 }) 63 } 64 } 65 66 func TestApplyF(t *testing.T) { 67 var tests = []struct { 68 name string 69 file string 70 count int 71 isError bool 72 expectedLastAppliedConfigAnnotations []string 73 }{ 74 { 75 "should apply file", 76 objects + "/service.yaml", 77 1, 78 false, 79 []string{"{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"name\":\"my-service\",\"namespace\":\"test\"},\"spec\":{\"ports\":[{\"port\":80,\"protocol\":\"TCP\",\"targetPort\":9376}],\"selector\":{\"app\":\"MyApp\"}}}\n"}, 80 }, 81 { 82 "should apply file with two objects", 83 testdata + "/two_objects.yaml", 84 2, 85 false, 86 []string{"{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"name\":\"service1\",\"namespace\":\"test\"},\"spec\":{\"ports\":[{\"port\":80,\"protocol\":\"TCP\",\"targetPort\":9376}],\"selector\":{\"app\":\"MyApp\"}}}\n", 87 "{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"name\":\"service2\",\"namespace\":\"test\"},\"spec\":{\"ports\":[{\"port\":80,\"protocol\":\"TCP\",\"targetPort\":9376}],\"selector\":{\"app\":\"MyApp\"}}}\n"}, 88 }, 89 { 90 "should fail to apply files that are not YAML", 91 "blahblah", 92 0, 93 true, 94 nil, 95 }, 96 { 97 "should fail when file is not YAML", 98 objects + "/not-yaml.txt", 99 0, 100 true, 101 nil, 102 }, 103 } 104 105 for _, tt := range tests { 106 t.Run(tt.name, func(t *testing.T) { 107 c := fake.NewClientBuilder().WithScheme(k8scheme.Scheme).Build() 108 y := k8sutil.NewYAMLApplier(c, "test") 109 err := y.ApplyF(tt.file) 110 if tt.isError { 111 assert.Error(t, err) 112 } else { 113 assert.NoError(t, err) 114 } 115 assert.Equal(t, tt.count, len(y.Objects())) 116 117 for i, actualObj := range y.Objects() { 118 actual := actualObj.GetAnnotations()[corev1.LastAppliedConfigAnnotation] 119 expected := tt.expectedLastAppliedConfigAnnotations[i] 120 if diff := cmp.Diff(actual, expected); diff != "" { 121 t.Errorf("expected %v\n, got %v instead", expected, actual) 122 t.Logf("Difference: %s", diff) 123 } 124 } 125 }) 126 } 127 } 128 129 // TestApplyFNonSpec 130 // GIVEN a object that contains top level fields outside of spec 131 // 132 // WHEN I call apply with changes non-spec fields 133 // THEN the resulting object contains the updates 134 func TestApplyFNonSpec(t *testing.T) { 135 sa := &corev1.ServiceAccount{ 136 ObjectMeta: metav1.ObjectMeta{ 137 Name: constants.VerrazzanoPlatformOperator, 138 Namespace: constants.VerrazzanoInstall, 139 }, 140 Secrets: []corev1.ObjectReference{ 141 { 142 Name: "verrazzano-platform-operator-token", 143 }, 144 }, 145 } 146 c := fake.NewClientBuilder().WithScheme(helpers.NewScheme()).WithObjects(sa).Build() 147 y := k8sutil.NewYAMLApplier(c, "") 148 err := y.ApplyF(testdata + "/sa_add_imagepullsecrets.yaml") 149 assert.NoError(t, err) 150 151 // Verify the resulting SA 152 saUpdated := &corev1.ServiceAccount{} 153 err = c.Get(context.TODO(), types.NamespacedName{Name: constants.VerrazzanoPlatformOperator, Namespace: constants.VerrazzanoInstall}, saUpdated) 154 assert.NoError(t, err) 155 156 assert.NotEmpty(t, saUpdated.ImagePullSecrets) 157 assert.Equal(t, 1, len(saUpdated.ImagePullSecrets)) 158 assert.Equal(t, "verrazzano-container-registry", saUpdated.ImagePullSecrets[0].Name) 159 160 assert.Empty(t, saUpdated.Secrets) 161 assert.Equal(t, 0, len(saUpdated.Secrets)) 162 } 163 164 // TestApplyFMerge 165 // GIVEN a object that contains spec field 166 // 167 // WHEN I call apply with additions to the spec field 168 // THEN the resulting object contains the merged updates 169 func TestApplyFMerge(t *testing.T) { 170 deadlineSeconds := int32(5) 171 deployment := &appv1.Deployment{ 172 ObjectMeta: metav1.ObjectMeta{ 173 Name: constants.VerrazzanoPlatformOperator, 174 Namespace: constants.VerrazzanoInstall, 175 }, 176 Spec: appv1.DeploymentSpec{ 177 MinReadySeconds: 5, 178 ProgressDeadlineSeconds: &deadlineSeconds, 179 }, 180 } 181 c := fake.NewClientBuilder().WithScheme(helpers.NewScheme()).WithObjects(deployment).Build() 182 y := k8sutil.NewYAMLApplier(c, "") 183 err := y.ApplyF(testdata + "/deployment_merge.yaml") 184 assert.NoError(t, err) 185 186 // Verify the resulting Deployment 187 depUpdated := &appv1.Deployment{} 188 err = c.Get(context.TODO(), types.NamespacedName{Name: constants.VerrazzanoPlatformOperator, Namespace: constants.VerrazzanoInstall}, depUpdated) 189 assert.NoError(t, err) 190 191 assert.Equal(t, int32(5), depUpdated.Spec.MinReadySeconds) 192 assert.Equal(t, int32(5), *depUpdated.Spec.Replicas) 193 assert.Equal(t, int32(10), *depUpdated.Spec.ProgressDeadlineSeconds) 194 } 195 196 // TestApplyFClusterRole 197 // GIVEN a ClusterRole object 198 // 199 // WHEN I call apply with additions 200 // THEN the resulting object contains the merged updates 201 func TestApplyFClusterRole(t *testing.T) { 202 deadlineSeconds := int32(5) 203 deployment := &appv1.Deployment{ 204 ObjectMeta: metav1.ObjectMeta{ 205 Name: constants.VerrazzanoPlatformOperator, 206 Namespace: constants.VerrazzanoInstall, 207 }, 208 Spec: appv1.DeploymentSpec{ 209 MinReadySeconds: 5, 210 ProgressDeadlineSeconds: &deadlineSeconds, 211 }, 212 } 213 c := fake.NewClientBuilder().WithScheme(helpers.NewScheme()).WithObjects(deployment).Build() 214 y := k8sutil.NewYAMLApplier(c, "") 215 err := y.ApplyF(testdata + "/clusterrole_create.yaml") 216 assert.NoError(t, err) 217 218 // Verify the ClusterRole that was created 219 clusterRole := &rbacv1.ClusterRole{} 220 err = c.Get(context.TODO(), types.NamespacedName{Name: "verrazzano-managed-cluster"}, clusterRole) 221 assert.NoError(t, err) 222 223 assert.Equal(t, 1, len(clusterRole.Rules)) 224 rule := clusterRole.Rules[0] 225 assert.Equal(t, "", rule.APIGroups[0]) 226 assert.Equal(t, "secrets", rule.Resources[0]) 227 assert.Equal(t, 3, len(rule.Verbs)) 228 229 // Update the ClusterRole 230 err = y.ApplyF(testdata + "/clusterrole_update.yaml") 231 assert.NoError(t, err) 232 233 // Verify the ClusterRole that was updated 234 clusterRoleUpdated := &rbacv1.ClusterRole{} 235 err = c.Get(context.TODO(), types.NamespacedName{Name: "verrazzano-managed-cluster"}, clusterRoleUpdated) 236 assert.NoError(t, err) 237 rule = clusterRoleUpdated.Rules[0] 238 assert.Equal(t, 4, len(rule.Verbs)) 239 240 // Verify all the expected verbs are there 241 foundCount := 0 242 for _, verb := range rule.Verbs { 243 switch verb { 244 case "get": 245 foundCount++ 246 case "list": 247 foundCount++ 248 case "watch": 249 foundCount++ 250 case "update": 251 foundCount++ 252 } 253 } 254 assert.Equal(t, 4, foundCount) 255 } 256 257 func TestApplyFT(t *testing.T) { 258 var tests = []struct { 259 name string 260 file string 261 args map[string]interface{} 262 count int 263 isError bool 264 expectedLastAppliedConfigAnnotations []string 265 }{ 266 { 267 "should apply a template file", 268 testdata + "/templated_service.yaml", 269 map[string]interface{}{"namespace": "default"}, 270 1, 271 false, 272 []string{"{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"name\":\"tmpl-service\",\"namespace\":\"default\"},\"spec\":{\"ports\":[{\"port\":80,\"protocol\":\"TCP\",\"targetPort\":9376}],\"selector\":{\"app\":\"MyApp\"}}}\n"}, 273 }, 274 { 275 "should fail to apply when template is incomplete", 276 testdata + "/templated_service.yaml", 277 map[string]interface{}{}, 278 0, 279 true, 280 nil, 281 }, 282 } 283 284 for _, tt := range tests { 285 t.Run(tt.name, func(t *testing.T) { 286 c := fake.NewClientBuilder().WithScheme(k8scheme.Scheme).Build() 287 y := k8sutil.NewYAMLApplier(c, "") 288 err := y.ApplyFT(tt.file, tt.args) 289 if tt.isError { 290 assert.Error(t, err) 291 } else { 292 assert.NoError(t, err) 293 } 294 assert.Equal(t, tt.count, len(y.Objects())) 295 296 for i, actualObj := range y.Objects() { 297 actual := actualObj.GetAnnotations()[corev1.LastAppliedConfigAnnotation] 298 assert.NotEmpty(t, actual) 299 expected := tt.expectedLastAppliedConfigAnnotations[i] 300 if diff := cmp.Diff(actual, expected); diff != "" { 301 t.Errorf("expected %v\n, got %v instead", expected, actual) 302 t.Logf("Difference: %s", diff) 303 } 304 } 305 }) 306 } 307 } 308 309 // TestApplyDT tests the ApplyDT function. 310 func TestApplyDT(t *testing.T) { 311 var tests = []struct { 312 name string 313 dir string 314 args map[string]interface{} 315 count int 316 isError bool 317 }{ 318 // GIVEN a directory of template YAML files 319 // WHEN the ApplyDT function is called with substitution key/value pairs 320 // THEN the call succeeds and the resources are applied to the cluster 321 { 322 "should apply all template files in directory", 323 testdata, 324 map[string]interface{}{"namespace": "default"}, 325 7, 326 false, 327 }, 328 // GIVEN a directory of template YAML files 329 // WHEN the ApplyDT function is called with no substitution key/value pairs 330 // THEN the call fails 331 { 332 "should fail to apply when one or more templates are incomplete", 333 testdata, 334 map[string]interface{}{}, 335 4, 336 true, 337 }, 338 } 339 340 for _, tt := range tests { 341 t.Run(tt.name, func(t *testing.T) { 342 c := fake.NewClientBuilder().WithScheme(k8scheme.Scheme).Build() 343 y := k8sutil.NewYAMLApplier(c, "") 344 err := y.ApplyDT(tt.dir, tt.args) 345 if tt.isError { 346 assert.Error(t, err) 347 } else { 348 assert.NoError(t, err) 349 } 350 assert.Equal(t, tt.count, len(y.Objects())) 351 }) 352 } 353 } 354 355 func TestDeleteF(t *testing.T) { 356 var tests = []struct { 357 name string 358 file string 359 isError bool 360 }{ 361 { 362 "should delete valid file", 363 testdata + "/two_objects.yaml", 364 false, 365 }, 366 { 367 "should fail to delete invalid file", 368 "blahblah", 369 true, 370 }, 371 } 372 373 for _, tt := range tests { 374 t.Run(tt.name, func(t *testing.T) { 375 c := fake.NewClientBuilder().WithScheme(k8scheme.Scheme).Build() 376 y := k8sutil.NewYAMLApplier(c, "") 377 err := y.DeleteF(tt.file) 378 if tt.isError { 379 assert.Error(t, err) 380 } else { 381 assert.NoError(t, err) 382 } 383 }) 384 } 385 } 386 387 func TestDeleteFD(t *testing.T) { 388 var tests = []struct { 389 name string 390 file string 391 args map[string]interface{} 392 isError bool 393 }{ 394 { 395 "should apply a template file", 396 testdata + "/templated_service.yaml", 397 map[string]interface{}{"namespace": "default"}, 398 false, 399 }, 400 { 401 "should fail to apply when template is incomplete", 402 testdata + "/templated_service.yaml", 403 map[string]interface{}{}, 404 true, 405 }, 406 } 407 408 for _, tt := range tests { 409 t.Run(tt.name, func(t *testing.T) { 410 c := fake.NewClientBuilder().WithScheme(k8scheme.Scheme).Build() 411 y := k8sutil.NewYAMLApplier(c, "") 412 err := y.DeleteFT(tt.file, tt.args) 413 if tt.isError { 414 assert.Error(t, err) 415 } else { 416 assert.NoError(t, err) 417 } 418 }) 419 } 420 } 421 422 // TestDeleteFTDefaultConfig tests deleteFT with rest client from the default config 423 // GIVEN a filepath and args 424 // 425 // WHEN TestDeleteFTDefaultConfig is called 426 // THEN it fails to get the default restclient 427 //func TestDeleteFTDefaultConfig(t *testing.T) { 428 // var tests = []struct { 429 // name string 430 // file string 431 // args map[string]interface{} 432 // isError bool 433 // }{ 434 // { 435 // "should fail to delete a template file", 436 // testdata + "/templated_service.yaml", 437 // map[string]interface{}{"namespace": "default"}, 438 // true, 439 // }, 440 // } 441 // 442 // for _, tt := range tests { 443 // t.Run(tt.name, func(t *testing.T) { 444 // c := fake.NewClientBuilder().WithScheme(k8scheme.Scheme).Build() 445 // y := k8sutil.NewYAMLApplier(c, "") 446 // err := y.DeleteFTDefaultConfig(tt.file, tt.args) 447 // if tt.isError { 448 // assert.Error(t, err) 449 // } else { 450 // assert.NoError(t, err) 451 // } 452 // }) 453 // } 454 //} 455 456 func TestDeleteAll(t *testing.T) { 457 c := fake.NewClientBuilder().WithScheme(k8scheme.Scheme).Build() 458 y := k8sutil.NewYAMLApplier(c, "") 459 err := y.ApplyD(objects) 460 assert.NoError(t, err) 461 assert.Equal(t, 3, len(y.Objects())) 462 err = y.DeleteAll() 463 assert.NoError(t, err) 464 assert.Equal(t, 0, len(y.Objects())) 465 }