github.com/redhat-appstudio/release-service@v0.0.0-20240507143925-083712697924/api/v1alpha1/webhooks/author/webhook_test.go (about) 1 // 2 // Copyright 2022 Red Hat, Inc. 3 // 4 // Licensed under the Apache License, Version 2.0 (the "License"); 5 // you may not use this file except in compliance with the License. 6 // You may obtain a copy of the License at 7 // 8 // http://www.apache.org/licenses/LICENSE-2.0 9 // 10 // Unless required by applicable law or agreed to in writing, software 11 // distributed under the License is distributed on an "AS IS" BASIS, 12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 // See the License for the specific language governing permissions and 14 // limitations under the License. 15 16 package author 17 18 import ( 19 "encoding/json" 20 "github.com/redhat-appstudio/release-service/api/v1alpha1" 21 "net/http" 22 23 . "github.com/onsi/ginkgo/v2" 24 . "github.com/onsi/gomega" 25 "github.com/redhat-appstudio/release-service/metadata" 26 "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 27 28 admissionv1 "k8s.io/api/admission/v1" 29 corev1 "k8s.io/api/core/v1" 30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 //+kubebuilder:scaffold:imports 32 ) 33 34 var _ = Describe("Author webhook", Ordered, func() { 35 var admissionRequest admission.Request 36 var err error 37 38 BeforeAll(func() { 39 admissionRequest.UserInfo.Username = "admin" 40 }) 41 42 Describe("A Release request is made", func() { 43 var release *v1alpha1.Release 44 45 BeforeEach(func() { 46 admissionRequest.Kind.Kind = "Release" 47 48 release = &v1alpha1.Release{ 49 TypeMeta: metav1.TypeMeta{ 50 APIVersion: "appstudio.redhat.com/v1alpha1", 51 Kind: "Release", 52 }, 53 ObjectMeta: metav1.ObjectMeta{ 54 Name: "test-release", 55 Namespace: "default", 56 }, 57 Spec: v1alpha1.ReleaseSpec{ 58 Snapshot: "test-snapshot", 59 ReleasePlan: "test-releaseplan", 60 }, 61 } 62 }) 63 64 When("a Release is created", func() { 65 BeforeAll(func() { 66 admissionRequest.AdmissionRequest.Operation = admissionv1.Create 67 }) 68 69 It("should add admin as the value for the author label", func() { 70 admissionRequest.Object.Raw, err = json.Marshal(release) 71 Expect(err).NotTo(HaveOccurred()) 72 73 rsp := webhook.Handle(ctx, admissionRequest) 74 Expect(rsp.AdmissionResponse.Allowed).To(BeTrue()) 75 Expect(len(rsp.Patches)).To(Equal(1)) 76 patch := rsp.Patches[0] 77 Expect(patch.Operation).To(Equal("add")) 78 Expect(patch.Path).To(Equal("/metadata/labels")) 79 Expect(patch.Value).To(Equal(map[string]interface{}{ 80 metadata.AuthorLabel: "admin", 81 })) 82 }) 83 84 It("should overwrite the author label value when one is provided by user", func() { 85 releaseDifferentAuthor := &v1alpha1.Release{ 86 TypeMeta: metav1.TypeMeta{ 87 APIVersion: "appstudio.redhat.com/v1alpha1", 88 Kind: "Release", 89 }, 90 ObjectMeta: metav1.ObjectMeta{ 91 Name: "test-release", 92 Namespace: "default", 93 Labels: map[string]string{ 94 metadata.AuthorLabel: "user", 95 }, 96 }, 97 Spec: v1alpha1.ReleaseSpec{ 98 Snapshot: "test-snapshot", 99 ReleasePlan: "test-releaseplan", 100 }, 101 } 102 103 admissionRequest.Object.Raw, err = json.Marshal(releaseDifferentAuthor) 104 Expect(err).NotTo(HaveOccurred()) 105 106 rsp := webhook.Handle(ctx, admissionRequest) 107 Expect(rsp.AdmissionResponse.Allowed).To(BeTrue()) 108 Expect(len(rsp.Patches)).To(Equal(1)) 109 patch := rsp.Patches[0] 110 Expect(patch.Operation).To(Equal("replace")) 111 // The json functions replace `/` so checking the entire value does not work 112 Expect(patch.Path).To(ContainSubstring("author")) 113 Expect(patch.Value).To(Equal("admin")) 114 }) 115 116 It("should not add the author label if the automated label is present and true", func() { 117 release.Labels = map[string]string{ 118 metadata.AutomatedLabel: "true", 119 } 120 admissionRequest.Object.Raw, err = json.Marshal(release) 121 Expect(err).NotTo(HaveOccurred()) 122 123 rsp := webhook.Handle(ctx, admissionRequest) 124 Expect(rsp.AdmissionResponse.Allowed).To(BeTrue()) 125 Expect(rsp.AdmissionResponse.Patch).To(BeNil()) 126 Expect(len(rsp.Patches)).To(Equal(0)) 127 }) 128 129 It("should add the author label if the automated label is false", func() { 130 release.Labels = map[string]string{ 131 metadata.AutomatedLabel: "false", 132 } 133 admissionRequest.Object.Raw, err = json.Marshal(release) 134 Expect(err).NotTo(HaveOccurred()) 135 136 rsp := webhook.Handle(ctx, admissionRequest) 137 Expect(rsp.AdmissionResponse.Allowed).To(BeTrue()) 138 Expect(len(rsp.Patches)).To(Equal(1)) 139 patch := rsp.Patches[0] 140 Expect(patch.Operation).To(Equal("add")) 141 // The json functions replace `/` so checking the entire value does not work 142 Expect(patch.Path).To(ContainSubstring("author")) 143 Expect(patch.Value).To(Equal("admin")) 144 }) 145 }) 146 147 When("a Release is updated", func() { 148 BeforeAll(func() { 149 admissionRequest.AdmissionRequest.Operation = admissionv1.Update 150 }) 151 152 It("should allow changes to metadata besides the author label", func() { 153 release.ObjectMeta.Labels = map[string]string{ 154 metadata.AuthorLabel: "admin", 155 } 156 releaseMetadataChange := &v1alpha1.Release{ 157 TypeMeta: metav1.TypeMeta{ 158 APIVersion: "appstudio.redhat.com/v1alpha1", 159 Kind: "Release", 160 }, 161 ObjectMeta: metav1.ObjectMeta{ 162 Name: "test-release", 163 Namespace: "default", 164 Labels: map[string]string{ 165 metadata.AuthorLabel: "admin", 166 }, 167 Annotations: map[string]string{ 168 "foo": "bar", 169 }, 170 }, 171 Spec: v1alpha1.ReleaseSpec{ 172 Snapshot: "test-snapshot", 173 ReleasePlan: "test-releaseplan", 174 }, 175 } 176 177 admissionRequest.Object.Raw, err = json.Marshal(release) 178 Expect(err).NotTo(HaveOccurred()) 179 admissionRequest.OldObject.Raw, err = json.Marshal(releaseMetadataChange) 180 Expect(err).NotTo(HaveOccurred()) 181 182 rsp := webhook.Handle(ctx, admissionRequest) 183 Expect(rsp.AdmissionResponse.Allowed).To(BeTrue()) 184 Expect(rsp.AdmissionResponse.Result.Code).To(Equal(int32(http.StatusOK))) 185 Expect(rsp.AdmissionResponse.Result.Message).To(Equal(metav1.StatusSuccess)) 186 }) 187 188 It("should not allow the author label to be set to a different value", func() { 189 releaseMetadataChange := &v1alpha1.Release{ 190 TypeMeta: metav1.TypeMeta{ 191 APIVersion: "appstudio.redhat.com/v1alpha1", 192 Kind: "Release", 193 }, 194 ObjectMeta: metav1.ObjectMeta{ 195 Name: "test-release", 196 Namespace: "default", 197 Labels: map[string]string{ 198 metadata.AuthorLabel: "user", 199 }, 200 }, 201 Spec: v1alpha1.ReleaseSpec{ 202 Snapshot: "test-snapshot", 203 ReleasePlan: "test-releaseplan", 204 }, 205 } 206 207 admissionRequest.Object.Raw, err = json.Marshal(release) 208 Expect(err).NotTo(HaveOccurred()) 209 admissionRequest.OldObject.Raw, err = json.Marshal(releaseMetadataChange) 210 Expect(err).NotTo(HaveOccurred()) 211 212 rsp := webhook.Handle(ctx, admissionRequest) 213 Expect(rsp.AdmissionResponse.Allowed).To(BeFalse()) 214 Expect(rsp.AdmissionResponse.Result).To(Equal(&metav1.Status{ 215 Code: http.StatusBadRequest, 216 Message: "release author label cannnot be updated", 217 })) 218 }) 219 }) 220 }) 221 222 Describe("A ReleasePlan request is made", func() { 223 var releasePlan *v1alpha1.ReleasePlan 224 225 BeforeEach(func() { 226 admissionRequest.Kind.Kind = "ReleasePlan" 227 228 releasePlan = &v1alpha1.ReleasePlan{ 229 TypeMeta: metav1.TypeMeta{ 230 APIVersion: "appstudio.redhat.com/v1alpha1", 231 Kind: "ReleasePlan", 232 }, 233 ObjectMeta: metav1.ObjectMeta{ 234 Name: "test-releaseplan", 235 Namespace: "default", 236 }, 237 Spec: v1alpha1.ReleasePlanSpec{ 238 Application: "test-application", 239 Target: "test-target", 240 }, 241 } 242 }) 243 244 When("a ReleasePlan is created", func() { 245 BeforeAll(func() { 246 admissionRequest.AdmissionRequest.Operation = admissionv1.Create 247 }) 248 249 It("should add admin as the value for the author label if attribution is set to true", func() { 250 releasePlan.Labels = map[string]string{ 251 metadata.AttributionLabel: "true", 252 } 253 admissionRequest.Object.Raw, err = json.Marshal(releasePlan) 254 Expect(err).NotTo(HaveOccurred()) 255 256 rsp := webhook.Handle(ctx, admissionRequest) 257 Expect(rsp.AdmissionResponse.Allowed).To(BeTrue()) 258 Expect(len(rsp.Patches)).To(Equal(1)) 259 patch := rsp.Patches[0] 260 Expect(patch.Operation).To(Equal("add")) 261 // The json functions replace `/` so checking the entire value does not work 262 Expect(patch.Path).To(ContainSubstring("author")) 263 Expect(patch.Value).To(Equal("admin")) 264 }) 265 266 It("should allow the operation with no patch if the Attribution label is missing", func() { 267 admissionRequest.Object.Raw, err = json.Marshal(releasePlan) 268 Expect(err).NotTo(HaveOccurred()) 269 270 rsp := webhook.Handle(ctx, admissionRequest) 271 Expect(rsp.AdmissionResponse.Allowed).To(BeTrue()) 272 Expect(rsp.AdmissionResponse.Patch).To(BeNil()) 273 Expect(len(rsp.Patches)).To(Equal(0)) 274 }) 275 276 It("should allow the operation with no patch if the Attribution label is false", func() { 277 releasePlan.Labels = map[string]string{ 278 metadata.AttributionLabel: "false", 279 } 280 admissionRequest.Object.Raw, err = json.Marshal(releasePlan) 281 Expect(err).NotTo(HaveOccurred()) 282 283 rsp := webhook.Handle(ctx, admissionRequest) 284 Expect(rsp.AdmissionResponse.Allowed).To(BeTrue()) 285 Expect(rsp.AdmissionResponse.Patch).To(BeNil()) 286 Expect(len(rsp.Patches)).To(Equal(0)) 287 }) 288 }) 289 290 When("a ReleasePlan is updated", func() { 291 var previousReleasePlan *v1alpha1.ReleasePlan 292 293 BeforeEach(func() { 294 admissionRequest.AdmissionRequest.Operation = admissionv1.Update 295 previousReleasePlan = &v1alpha1.ReleasePlan{ 296 TypeMeta: metav1.TypeMeta{ 297 APIVersion: "appstudio.redhat.com/v1alpha1", 298 Kind: "ReleasePlan", 299 }, 300 ObjectMeta: metav1.ObjectMeta{ 301 Name: "previous-releaseplan", 302 Namespace: "default", 303 }, 304 Spec: v1alpha1.ReleasePlanSpec{ 305 Application: "test-application", 306 Target: "test-target", 307 }, 308 } 309 }) 310 311 When("the Attribution label goes from true to true", func() { 312 BeforeEach(func() { 313 previousReleasePlan.Labels = map[string]string{ 314 metadata.AttributionLabel: "true", 315 } 316 releasePlan.Labels = map[string]string{ 317 metadata.AttributionLabel: "true", 318 } 319 }) 320 321 It("should maintain author value if trying to set it to a different user", func() { 322 previousReleasePlan.Labels[metadata.AuthorLabel] = "admin" 323 releasePlan.Labels[metadata.AuthorLabel] = "user" 324 admissionRequest.Object.Raw, err = json.Marshal(releasePlan) 325 Expect(err).NotTo(HaveOccurred()) 326 admissionRequest.OldObject.Raw, err = json.Marshal(previousReleasePlan) 327 Expect(err).NotTo(HaveOccurred()) 328 329 rsp := webhook.Handle(ctx, admissionRequest) 330 Expect(rsp.AdmissionResponse.Allowed).To(BeTrue()) 331 Expect(rsp.AdmissionResponse.Patch).To(BeNil()) 332 Expect(len(rsp.Patches)).To(Equal(1)) 333 patch := rsp.Patches[0] 334 Expect(patch.Operation).To(Equal("replace")) 335 // The json functions replace `/` so checking the entire value does not work 336 Expect(patch.Path).To(ContainSubstring("author")) 337 Expect(patch.Value).To(Equal("admin")) 338 }) 339 340 It("should allow the change if author value is not modified", func() { 341 previousReleasePlan.Labels[metadata.AuthorLabel] = "user" 342 releasePlan.Labels[metadata.AuthorLabel] = "user" 343 admissionRequest.Object.Raw, err = json.Marshal(releasePlan) 344 Expect(err).NotTo(HaveOccurred()) 345 admissionRequest.OldObject.Raw, err = json.Marshal(previousReleasePlan) 346 Expect(err).NotTo(HaveOccurred()) 347 348 rsp := webhook.Handle(ctx, admissionRequest) 349 Expect(rsp.AdmissionResponse.Allowed).To(BeTrue()) 350 Expect(rsp.AdmissionResponse.Patch).To(BeNil()) 351 Expect(len(rsp.Patches)).To(Equal(0)) 352 }) 353 354 It("should allow changing the author to the current user", func() { 355 previousReleasePlan.Labels[metadata.AuthorLabel] = "user" 356 releasePlan.Labels[metadata.AuthorLabel] = "admin" 357 admissionRequest.Object.Raw, err = json.Marshal(releasePlan) 358 Expect(err).NotTo(HaveOccurred()) 359 admissionRequest.OldObject.Raw, err = json.Marshal(previousReleasePlan) 360 Expect(err).NotTo(HaveOccurred()) 361 362 rsp := webhook.Handle(ctx, admissionRequest) 363 Expect(rsp.AdmissionResponse.Allowed).To(BeTrue()) 364 Expect(rsp.AdmissionResponse.Patch).To(BeNil()) 365 Expect(len(rsp.Patches)).To(Equal(0)) 366 }) 367 }) 368 369 When("the Attribution label goes to true to false", func() { 370 BeforeAll(func() { 371 previousReleasePlan.Labels = map[string]string{ 372 metadata.AttributionLabel: "true", 373 metadata.AuthorLabel: "admin", 374 } 375 releasePlan.Labels = map[string]string{ 376 metadata.AttributionLabel: "false", 377 metadata.AuthorLabel: "admin", 378 } 379 }) 380 381 It("should allow the change and remove the author label", func() { 382 admissionRequest.Object.Raw, err = json.Marshal(releasePlan) 383 Expect(err).NotTo(HaveOccurred()) 384 admissionRequest.OldObject.Raw, err = json.Marshal(previousReleasePlan) 385 Expect(err).NotTo(HaveOccurred()) 386 387 rsp := webhook.Handle(ctx, admissionRequest) 388 Expect(rsp.AdmissionResponse.Allowed).To(BeTrue()) 389 390 Expect(len(rsp.Patches)).To(Equal(1)) 391 patch := rsp.Patches[0] 392 Expect(patch.Operation).To(Equal("remove")) 393 // The json functions replace `/` so checking the entire value does not work 394 Expect(patch.Path).To(ContainSubstring("author")) 395 }) 396 }) 397 398 When("the Attribution label goes to false to true", func() { 399 BeforeEach(func() { 400 previousReleasePlan.Labels = map[string]string{ 401 metadata.AttributionLabel: "false", 402 } 403 releasePlan.Labels = map[string]string{ 404 metadata.AttributionLabel: "true", 405 } 406 }) 407 408 It("should set the author to be current user if provided different user", func() { 409 releasePlan.Labels[metadata.AuthorLabel] = "user" 410 admissionRequest.Object.Raw, err = json.Marshal(releasePlan) 411 Expect(err).NotTo(HaveOccurred()) 412 admissionRequest.OldObject.Raw, err = json.Marshal(previousReleasePlan) 413 Expect(err).NotTo(HaveOccurred()) 414 415 rsp := webhook.Handle(ctx, admissionRequest) 416 Expect(rsp.AdmissionResponse.Allowed).To(BeTrue()) 417 Expect(len(rsp.Patches)).To(Equal(1)) 418 patch := rsp.Patches[0] 419 Expect(patch.Operation).To(Equal("replace")) 420 // The json functions replace `/` so checking the entire value does not work 421 Expect(patch.Path).To(ContainSubstring("author")) 422 Expect(patch.Value).To(Equal("admin")) 423 }) 424 }) 425 426 When("the Attribution label goes to false to false", func() { 427 BeforeAll(func() { 428 previousReleasePlan.Labels = map[string]string{ 429 metadata.AttributionLabel: "false", 430 } 431 releasePlan.Labels = map[string]string{ 432 metadata.AttributionLabel: "false", 433 } 434 }) 435 436 It("should allow the change", func() { 437 admissionRequest.Object.Raw, err = json.Marshal(releasePlan) 438 Expect(err).NotTo(HaveOccurred()) 439 admissionRequest.OldObject.Raw, err = json.Marshal(previousReleasePlan) 440 Expect(err).NotTo(HaveOccurred()) 441 442 rsp := webhook.Handle(ctx, admissionRequest) 443 Expect(rsp.AdmissionResponse.Allowed).To(BeTrue()) 444 Expect(rsp.AdmissionResponse.Patch).To(BeNil()) 445 Expect(len(rsp.Patches)).To(Equal(0)) 446 }) 447 }) 448 449 It("should allow changes when releaseplans have no labels", func() { 450 releasePlan.Labels = nil 451 previousReleasePlan.Labels = nil 452 admissionRequest.Object.Raw, err = json.Marshal(releasePlan) 453 Expect(err).NotTo(HaveOccurred()) 454 admissionRequest.OldObject.Raw, err = json.Marshal(previousReleasePlan) 455 Expect(err).NotTo(HaveOccurred()) 456 457 rsp := webhook.Handle(ctx, admissionRequest) 458 Expect(rsp.AdmissionResponse.Allowed).To(BeTrue()) 459 Expect(rsp.AdmissionResponse.Patch).To(BeNil()) 460 Expect(len(rsp.Patches)).To(Equal(0)) 461 }) 462 }) 463 }) 464 465 When("patchResponse is called", func() { 466 467 It("should return an admission response with a patch", func() { 468 pod := &corev1.Pod{ 469 ObjectMeta: metav1.ObjectMeta{}, 470 } 471 marshalledPod, err := json.Marshal(pod) 472 Expect(err).NotTo(HaveOccurred()) 473 pod.Labels = map[string]string{ 474 "foo": "bar", 475 } 476 477 rsp := webhook.patchResponse(marshalledPod, pod) 478 Expect(rsp.AdmissionResponse.Allowed).To(BeTrue()) 479 Expect(len(rsp.Patches)).To(Equal(1)) 480 patch := rsp.Patches[0] 481 Expect(patch.Operation).To(Equal("add")) 482 Expect(patch.Path).To(Equal("/metadata/labels")) 483 Expect(patch.Value).To(Equal(map[string]interface{}{ 484 "foo": "bar", 485 })) 486 }) 487 }) 488 489 When("setAuthorLabel is called", func() { 490 491 It("should add the author label", func() { 492 pod := &corev1.Pod{ 493 ObjectMeta: metav1.ObjectMeta{ 494 Labels: map[string]string{ 495 "foo": "bar", 496 }, 497 }, 498 } 499 webhook.setAuthorLabel("admin", pod) 500 Expect(pod.GetLabels()).To(Equal(map[string]string{ 501 metadata.AuthorLabel: "admin", 502 "foo": "bar", 503 })) 504 }) 505 506 It("should add the author label if object has no existing labels", func() { 507 pod := &corev1.Pod{ 508 ObjectMeta: metav1.ObjectMeta{}, 509 } 510 webhook.setAuthorLabel("admin", pod) 511 Expect(pod.GetLabels()).To(Equal(map[string]string{ 512 metadata.AuthorLabel: "admin", 513 })) 514 }) 515 }) 516 517 When("sanitizeLabelValue is called", func() { 518 519 It("should convert : to _", func() { 520 str := webhook.sanitizeLabelValue("a:b") 521 Expect(str).To(Equal("a_b")) 522 }) 523 524 It("should trim long author values", func() { 525 str := webhook.sanitizeLabelValue("abcdefghijklmnopqrstuvwxyz_abcdefghijklmnopqrstuvwxyz_1234567890") 526 Expect(str).To(Equal("abcdefghijklmnopqrstuvwxyz_abcdefghijklmnopqrstuvwxyz_123456789")) 527 }) 528 }) 529 })