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  })