Skip to content

Commit

Permalink
adding a test case for validating webhook
Browse files Browse the repository at this point in the history
  • Loading branch information
Shawn Hurley committed Apr 4, 2022
1 parent ac1e4bb commit 16b708f
Showing 1 changed file with 230 additions and 0 deletions.
230 changes: 230 additions & 0 deletions test/e2e/conformance/webhook_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
package conformance

import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"path/filepath"
"sync"
"testing"
"time"

"github.com/kcp-dev/apimachinery/pkg/logicalcluster"
"github.com/kcp-dev/kcp/test/e2e/fixtures/wildwest"
"github.com/kcp-dev/kcp/test/e2e/fixtures/wildwest/apis/wildwest/v1alpha1"
client "github.com/kcp-dev/kcp/test/e2e/fixtures/wildwest/client/clientset/versioned"
"github.com/kcp-dev/kcp/test/e2e/framework"
"github.com/stretchr/testify/require"
v1 "k8s.io/api/admission/v1"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)

var scheme = runtime.NewScheme()

func init() {
admissionregistrationv1.AddToScheme(scheme)
v1.AddToScheme(scheme)
v1alpha1.AddToScheme(scheme)
}

func TestWebhookInWorkspace(t *testing.T) {
t.Parallel()

server := framework.SharedKcpServer(t)
dirPath := filepath.Dir(server.KubeconfigPath())

ctx, cancelFunc := context.WithCancel(context.Background())
t.Cleanup(cancelFunc)

cfg := server.DefaultConfig(t)
cfg.CertFile = filepath.Join(dirPath, "apiserver.crt")
cfg.KeyFile = filepath.Join(dirPath, "apiserver.key")

testWebhook := testWebhookServer{
Response: v1.AdmissionResponse{
Allowed: true,
},
ObjectGVK: schema.GroupVersionKind{
Group: "wildwest.dev",
Version: "v1alpha1",
Kind: "Cowboy",
},
T: t,
Lock: sync.Mutex{},
}

testWebhook.StartServer(ctx, cfg, 8090)

organization := framework.NewOrganizationFixture(t, server)
logicalClusters := []logicalcluster.LogicalCluster{
framework.NewWorkspaceFixture(t, server, organization, "Universal"),
framework.NewWorkspaceFixture(t, server, organization, "Universal"),
}

kubeClusterClient, err := kubernetes.NewClusterForConfig(cfg)
require.NoError(t, err, "failed to construct client for server")
cowbyClients, err := client.NewClusterForConfig(cfg)
require.NoError(t, err, "failed to construct cowboy client for server")

//Insall the Cowboy resources into logical clusters
for _, logicalCluster := range logicalClusters {
t.Logf("Bootstrapping ClusterWorkspace CRDs in logical cluster %s", logicalCluster)
apiExtensionsClients, err := apiextensionsclient.NewClusterForConfig(cfg)
require.NoError(t, err, "failed to construct apiextensions client for server")
crdClient := apiExtensionsClients.Cluster(logicalCluster).ApiextensionsV1().CustomResourceDefinitions()
wildwest.Create(t, crdClient, metav1.GroupResource{Group: "wildwest.dev", Resource: "cowboys"})
}
// Installing webhook into the first workspace.
url := "https://localhost:8090/hello"
sideEffect := admissionregistrationv1.SideEffectClassNone
webhook := &admissionregistrationv1.ValidatingWebhookConfiguration{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{Name: "test-webhook"},
Webhooks: []admissionregistrationv1.ValidatingWebhook{{
Name: "test-webhook.cowboy.io",
ClientConfig: admissionregistrationv1.WebhookClientConfig{
URL: &url,
CABundle: cfg.CAData,
},
Rules: []admissionregistrationv1.RuleWithOperations{{
Operations: []admissionregistrationv1.OperationType{
admissionregistrationv1.Create,
},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{"wildwest.dev"},
APIVersions: []string{"v1alpha1"},
Resources: []string{"cowboys"},
},
}},
SideEffects: &sideEffect,
AdmissionReviewVersions: []string{"v1"},
}},
}
_, err = kubeClusterClient.Cluster(logicalClusters[0]).AdmissionregistrationV1().ValidatingWebhookConfigurations().Create(ctx, webhook, metav1.CreateOptions{})
require.NoError(t, err, "failed to add validating webhook configurations")
cowboy := v1alpha1.Cowboy{
ObjectMeta: metav1.ObjectMeta{
Name: "testing",
Namespace: "default",
},
Spec: v1alpha1.CowboySpec{},
}
require.Eventually(t, func() bool {
_, err = cowbyClients.Cluster(logicalClusters[1]).WildwestV1alpha1().Cowboys("default").Create(ctx, &cowboy, metav1.CreateOptions{})
if err != nil && !errors.IsAlreadyExists(err) {
return false
}
return true

}, 10*time.Second, 2*time.Second)
_, err = cowbyClients.Cluster(logicalClusters[0]).WildwestV1alpha1().Cowboys("default").Create(ctx, &cowboy, metav1.CreateOptions{})
require.NoError(t, err, "failed to create cowboy resource in first logical cluster")
require.Eventually(t, func() bool {
return testWebhook.Calls == 1
}, time.Second, 10*time.Millisecond)

}

type testWebhookServer struct {
Response v1.AdmissionResponse
ObjectGVK schema.GroupVersionKind
T *testing.T
Calls int
Lock sync.Mutex
}

func (t *testWebhookServer) StartServer(ctx context.Context, cfg *rest.Config, port int) {
serv := &http.Server{Addr: fmt.Sprintf(":%d", port), Handler: t}
go func() {
<-ctx.Done()
fmt.Println("Shutting down the HTTP server...")
serv.Shutdown(ctx)
}()
go serv.ListenAndServeTLS(cfg.CertFile, cfg.KeyFile)
}

func (t *testWebhookServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
// Make sure that this is a request for the object that was set.
if req.Body == nil {
msg := "Expected request body to be non-empty"
t.T.Logf("%v", msg)
http.Error(resp, msg, http.StatusBadRequest)
}

data, err := ioutil.ReadAll(req.Body)
if err != nil {
msg := fmt.Sprintf("Request could not be decoded: %v", err)
t.T.Logf("%v", msg)
http.Error(resp, msg, http.StatusBadRequest)
}

// verify the content type is accurate
contentType := req.Header.Get("Content-Type")
if contentType != "application/json" {
msg := fmt.Sprintf("contentType=%s, expect application/json", contentType)
t.T.Logf("%v", msg)
http.Error(resp, msg, http.StatusBadRequest)
return
}

var codecs = serializer.NewCodecFactory(scheme)
deserializer := codecs.UniversalDeserializer()
obj, gvk, err := deserializer.Decode(data, nil, nil)
if err != nil {
t.T.Errorf("%v", err)
}

if *gvk != v1.SchemeGroupVersion.WithKind("AdmissionReview") {
msg := fmt.Sprintf("Expected AdmissionReview but got: %T", obj)
t.T.Logf("%v", msg)
http.Error(resp, msg, http.StatusBadRequest)
return
}
requestedAdmissionReview, ok := obj.(*v1.AdmissionReview)
if !ok {
//return an error
msg := fmt.Sprintf("Expected AdmissionReview but got: %T", obj)
t.T.Logf("%v", msg)
http.Error(resp, msg, http.StatusBadRequest)
return
}
obj, objGVK, err := deserializer.Decode(requestedAdmissionReview.Request.Object.Raw, nil, nil)
if err != nil {
t.T.Errorf("%v", err)
}
if t.ObjectGVK != *objGVK {
//return an error
msg := fmt.Sprintf("Expected ObjectGVK: %v but got: %T", t.ObjectGVK, obj)
t.T.Logf("%v", msg)
http.Error(resp, msg, http.StatusBadRequest)
return
}
responseAdmissionReview := &v1.AdmissionReview{
TypeMeta: requestedAdmissionReview.TypeMeta,
}
responseAdmissionReview.Response = &t.Response
responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID
respBytes, err := json.Marshal(responseAdmissionReview)
if err != nil {
t.T.Logf("%v", err)
http.Error(resp, err.Error(), http.StatusInternalServerError)
return
}
t.Lock.Lock()
t.Calls = t.Calls + 1
t.Lock.Unlock()
resp.Header().Set("Content-Type", "application/json")
if _, err := resp.Write(respBytes); err != nil {
t.T.Logf("%v", err)
}
}

0 comments on commit 16b708f

Please sign in to comment.