/* Copyright 2020 The Knative Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package webhook import ( "bytes" "context" "encoding/json" "fmt" "io/ioutil" "net/http" "net/url" "path" "strings" "testing" "github.com/mattbaird/jsonpatch" "golang.org/x/sync/errgroup" admissionv1beta1 "k8s.io/api/admission/v1beta1" authenticationv1 "k8s.io/api/authentication/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "knative.dev/pkg/metrics/metricstest" ) type fixedAdmissionController struct { path string response *admissionv1beta1.AdmissionResponse } var _ AdmissionController = (*fixedAdmissionController)(nil) func (fac *fixedAdmissionController) Path() string { return fac.path } func (fac *fixedAdmissionController) Admit(ctx context.Context, req *admissionv1beta1.AdmissionRequest) *admissionv1beta1.AdmissionResponse { return fac.response } func TestAdmissionEmptyRequestBody(t *testing.T) { c := &fixedAdmissionController{ path: "/bazinga", response: &admissionv1beta1.AdmissionResponse{}, } testEmptyRequestBody(t, c) } func TestAdmissionValidResponseForResource(t *testing.T) { ac := &fixedAdmissionController{ path: "/bazinga", response: &admissionv1beta1.AdmissionResponse{}, } wh, serverURL, ctx, cancel, err := testSetup(t, ac) if err != nil { t.Fatalf("testSetup() = %v", err) } eg, _ := errgroup.WithContext(ctx) eg.Go(func() error { return wh.Run(ctx.Done()) }) defer func() { cancel() if err := eg.Wait(); err != nil { t.Errorf("Unable to run controller: %s", err) } }() pollErr := waitForServerAvailable(t, serverURL, testTimeout) if pollErr != nil { t.Fatalf("waitForServerAvailable() = %v", err) } tlsClient, err := createSecureTLSClient(t, wh.Client, &wh.Options) if err != nil { t.Fatalf("createSecureTLSClient() = %v", err) } admissionreq := &admissionv1beta1.AdmissionRequest{ Operation: admissionv1beta1.Create, Kind: metav1.GroupVersionKind{ Group: "pkg.knative.dev", Version: "v1alpha1", Kind: "Resource", }, } testRev := createResource("testrev") marshaled, err := json.Marshal(testRev) if err != nil { t.Fatalf("Failed to marshal resource: %s", err) } admissionreq.Resource.Group = "pkg.knative.dev" admissionreq.Object.Raw = marshaled rev := &admissionv1beta1.AdmissionReview{ Request: admissionreq, } reqBuf := new(bytes.Buffer) err = json.NewEncoder(reqBuf).Encode(&rev) if err != nil { t.Fatalf("Failed to marshal admission review: %v", err) } u, err := url.Parse(fmt.Sprintf("https://%s", serverURL)) if err != nil { t.Fatalf("bad url %v", err) } u.Path = path.Join(u.Path, ac.Path()) req, err := http.NewRequest("GET", u.String(), reqBuf) if err != nil { t.Fatalf("http.NewRequest() = %v", err) } req.Header.Add("Content-Type", "application/json") response, err := tlsClient.Do(req) if err != nil { t.Fatalf("Failed to get response %v", err) } if got, want := response.StatusCode, http.StatusOK; got != want { t.Errorf("Response status code = %v, wanted %v", got, want) } defer response.Body.Close() responseBody, err := ioutil.ReadAll(response.Body) if err != nil { t.Fatalf("Failed to read response body %v", err) } reviewResponse := admissionv1beta1.AdmissionReview{} err = json.NewDecoder(bytes.NewReader(responseBody)).Decode(&reviewResponse) if err != nil { t.Fatalf("Failed to decode response: %v", err) } metricstest.CheckStatsReported(t, requestCountName, requestLatenciesName) } func TestAdmissionInvalidResponseForResource(t *testing.T) { expectedError := "everything is fine." ac := &fixedAdmissionController{ path: "/booger", response: MakeErrorStatus(expectedError), } wh, serverURL, ctx, cancel, err := testSetup(t, ac) if err != nil { t.Fatalf("testSetup() = %v", err) } eg, _ := errgroup.WithContext(ctx) eg.Go(func() error { return wh.Run(ctx.Done()) }) defer func() { cancel() if err := eg.Wait(); err != nil { t.Errorf("Unable to run controller: %s", err) } }() pollErr := waitForServerAvailable(t, serverURL, testTimeout) if pollErr != nil { t.Fatalf("waitForServerAvailable() = %v", err) } tlsClient, err := createSecureTLSClient(t, wh.Client, &wh.Options) if err != nil { t.Fatalf("createSecureTLSClient() = %v", err) } resource := createResource(testResourceName) resource.Spec.FieldWithValidation = "not the right value" marshaled, err := json.Marshal(resource) if err != nil { t.Fatalf("Failed to marshal resource: %s", err) } admissionreq := &admissionv1beta1.AdmissionRequest{ Operation: admissionv1beta1.Create, Kind: metav1.GroupVersionKind{ Group: "pkg.knative.dev", Version: "v1alpha1", Kind: "Resource", }, UserInfo: authenticationv1.UserInfo{ Username: user1, }, } admissionreq.Resource.Group = "pkg.knative.dev" admissionreq.Object.Raw = marshaled rev := &admissionv1beta1.AdmissionReview{ Request: admissionreq, } reqBuf := new(bytes.Buffer) err = json.NewEncoder(reqBuf).Encode(&rev) if err != nil { t.Fatalf("Failed to marshal admission review: %v", err) } u, err := url.Parse(fmt.Sprintf("https://%s", serverURL)) if err != nil { t.Fatalf("bad url %v", err) } u.Path = path.Join(u.Path, ac.Path()) req, err := http.NewRequest("GET", u.String(), reqBuf) if err != nil { t.Fatalf("http.NewRequest() = %v", err) } req.Header.Add("Content-Type", "application/json") response, err := tlsClient.Do(req) if err != nil { t.Fatalf("Failed to receive response %v", err) } if got, want := response.StatusCode, http.StatusOK; got != want { t.Errorf("Response status code = %v, wanted %v", got, want) } defer response.Body.Close() respBody, err := ioutil.ReadAll(response.Body) if err != nil { t.Fatalf("Failed to read response body %v", err) } reviewResponse := admissionv1beta1.AdmissionReview{} err = json.NewDecoder(bytes.NewReader(respBody)).Decode(&reviewResponse) if err != nil { t.Fatalf("Failed to decode response: %v", err) } var respPatch []jsonpatch.JsonPatchOperation err = json.Unmarshal(reviewResponse.Response.Patch, &respPatch) if err == nil { t.Fatalf("Expected to fail JSON unmarshal of resposnse") } if got, want := reviewResponse.Response.Result.Status, "Failure"; got != want { t.Errorf("Response status = %v, wanted %v", got, want) } if !strings.Contains(reviewResponse.Response.Result.Message, expectedError) { t.Errorf("Received unexpected response status message %s", reviewResponse.Response.Result.Message) } // Stats should be reported for requests that have admission disallowed metricstest.CheckStatsReported(t, requestCountName, requestLatenciesName) }