Skip to content

Commit

Permalink
admission/webhooks: set kcp.io/cluster annotation on create
Browse files Browse the repository at this point in the history
Signed-off-by: Dr. Stefan Schimanski <[email protected]>
  • Loading branch information
sttts committed May 2, 2024
1 parent 702ec3d commit c707d7f
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 20 deletions.
17 changes: 17 additions & 0 deletions pkg/admission/webhook/generic_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import (
"fmt"
"sync"

"github.com/kcp-dev/kcp/sdk/apis/core"
"github.com/kcp-dev/logicalcluster/v3"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"k8s.io/apimachinery/pkg/labels"
"k8s.io/apiserver/pkg/admission"
Expand Down Expand Up @@ -135,6 +137,21 @@ func (p *WebhookDispatcher) Dispatch(ctx context.Context, attr admission.Attribu
klog.FromContext(ctx).V(7).WithValues("cluster", lcluster).Info("restricting call to hooks in cluster")
}

// Add cluster annotation on create
if attr.GetOperation() == admission.Create {
u, ok := attr.GetObject().(metav1.Object)
if !ok {
return fmt.Errorf("unexpected type %T", attr.GetObject())
}

annotations := u.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[core.LogicalClusterPathAnnotationKey] = lcluster.String()
u.SetAnnotations(annotations)
}

return p.dispatcher.Dispatch(ctx, attr, o, whAccessor)
}

Expand Down
14 changes: 7 additions & 7 deletions test/e2e/apibinding/apibinding_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import (
"github.com/kcp-dev/logicalcluster/v3"
"github.com/stretchr/testify/require"

v1 "k8s.io/api/admission/v1"
admissionv1 "k8s.io/api/admission/v1"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -118,7 +118,7 @@ func TestAPIBindingMutatingWebhook(t *testing.T) {
scheme := runtime.NewScheme()
err = admissionregistrationv1.AddToScheme(scheme)
require.NoError(t, err, "failed to add admission registration v1 scheme")
err = v1.AddToScheme(scheme)
err = admissionv1.AddToScheme(scheme)
require.NoError(t, err, "failed to add admission v1 scheme")
err = v1alpha1.AddToScheme(scheme)
require.NoError(t, err, "failed to add cowboy v1alpha1 to scheme")
Expand All @@ -131,8 +131,8 @@ func TestAPIBindingMutatingWebhook(t *testing.T) {
testWebhooks := map[logicalcluster.Path]*webhookserver.AdmissionWebhookServer{}
for _, cluster := range []logicalcluster.Path{sourcePath, targetPath} {
testWebhooks[cluster] = &webhookserver.AdmissionWebhookServer{
Response: v1.AdmissionResponse{
Allowed: true,
ResponseFn: func(review *admissionv1.AdmissionReview) (*admissionv1.AdmissionResponse, error) {
return &admissionv1.AdmissionResponse{Allowed: true}, nil
},
ObjectGVK: schema.GroupVersionKind{
Group: "wildwest.dev",
Expand Down Expand Up @@ -265,7 +265,7 @@ func TestAPIBindingValidatingWebhook(t *testing.T) {
scheme := runtime.NewScheme()
err = admissionregistrationv1.AddToScheme(scheme)
require.NoError(t, err, "failed to add admission registration v1 scheme")
err = v1.AddToScheme(scheme)
err = admissionv1.AddToScheme(scheme)
require.NoError(t, err, "failed to add admission v1 scheme")
err = v1alpha1.AddToScheme(scheme)
require.NoError(t, err, "failed to add cowboy v1alpha1 to scheme")
Expand All @@ -278,8 +278,8 @@ func TestAPIBindingValidatingWebhook(t *testing.T) {
testWebhooks := map[logicalcluster.Path]*webhookserver.AdmissionWebhookServer{}
for _, cluster := range []logicalcluster.Path{sourcePath, targetPath} {
testWebhooks[cluster] = &webhookserver.AdmissionWebhookServer{
Response: v1.AdmissionResponse{
Allowed: true,
ResponseFn: func(review *admissionv1.AdmissionReview) (*admissionv1.AdmissionResponse, error) {
return &admissionv1.AdmissionResponse{Allowed: true}, nil
},
ObjectGVK: schema.GroupVersionKind{
Group: "wildwest.dev",
Expand Down
34 changes: 23 additions & 11 deletions test/e2e/conformance/webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,18 @@ package conformance

import (
"context"
"encoding/json"
"path/filepath"
"sync/atomic"
"testing"
"time"

kcpapiextensionsclientset "github.com/kcp-dev/client-go/apiextensions/client"
kcpkubernetesclientset "github.com/kcp-dev/client-go/kubernetes"
tenancyv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1"
"github.com/kcp-dev/logicalcluster/v3"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

v1 "k8s.io/api/admission/v1"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
Expand Down Expand Up @@ -66,9 +70,15 @@ func TestMutatingWebhookInWorkspace(t *testing.T) {
codecs := serializer.NewCodecFactory(scheme)
deserializer := codecs.UniversalDeserializer()

var clusterInReviewObject atomic.Value
testWebhook := webhookserver.AdmissionWebhookServer{
Response: v1.AdmissionResponse{
Allowed: true,
ResponseFn: func(review *v1.AdmissionReview) (*v1.AdmissionResponse, error) {
var u unstructured.Unstructured
if err := json.Unmarshal(review.Request.Object.Raw, &u); err != nil {
return nil, err
}
clusterInReviewObject.Store(logicalcluster.From(&u).String())
return &v1.AdmissionResponse{Allowed: true}, nil
},
ObjectGVK: schema.GroupVersionKind{
Group: "wildwest.dev",
Expand All @@ -84,9 +94,10 @@ func TestMutatingWebhookInWorkspace(t *testing.T) {
testWebhook.StartTLS(t, filepath.Join(dirPath, "apiserver.crt"), filepath.Join(dirPath, "apiserver.key"), port)

orgPath, _ := framework.NewOrganizationFixture(t, server)
ws1Path, _ := framework.NewWorkspaceFixture(t, server, orgPath)
ws2Path, _ := framework.NewWorkspaceFixture(t, server, orgPath)
workspaces := []logicalcluster.Path{ws1Path, ws2Path}
ws1Path, ws1 := framework.NewWorkspaceFixture(t, server, orgPath)
ws2Path, ws2 := framework.NewWorkspaceFixture(t, server, orgPath)
paths := []logicalcluster.Path{ws1Path, ws2Path}
workspaces := []*tenancyv1alpha1.Workspace{ws1, ws2}

kubeClusterClient, err := kcpkubernetesclientset.NewForConfig(cfg)
require.NoError(t, err, "failed to construct client for server")
Expand All @@ -96,7 +107,7 @@ func TestMutatingWebhookInWorkspace(t *testing.T) {
require.NoError(t, err, "failed to construct apiextensions client for server")

t.Logf("Install the Cowboy resources into logical clusters")
for _, wsPath := range workspaces {
for _, wsPath := range paths {
t.Logf("Bootstrapping Workspace CRDs in logical cluster %s", wsPath)
crdClient := apiExtensionsClients.ApiextensionsV1().CustomResourceDefinitions()
wildwest.Create(t, wsPath, crdClient, metav1.GroupResource{Group: "wildwest.dev", Resource: "cowboys"})
Expand Down Expand Up @@ -128,7 +139,7 @@ func TestMutatingWebhookInWorkspace(t *testing.T) {
AdmissionReviewVersions: []string{"v1"},
}},
}
_, err = kubeClusterClient.Cluster(workspaces[0]).AdmissionregistrationV1().MutatingWebhookConfigurations().Create(ctx, webhook, metav1.CreateOptions{})
_, err = kubeClusterClient.Cluster(paths[0]).AdmissionregistrationV1().MutatingWebhookConfigurations().Create(ctx, webhook, metav1.CreateOptions{})
require.NoError(t, err, "failed to add mutating webhook configurations")

cowboy := v1alpha1.Cowboy{
Expand All @@ -140,17 +151,18 @@ func TestMutatingWebhookInWorkspace(t *testing.T) {

t.Logf("Creating cowboy resource in first logical cluster")
require.Eventually(t, func() bool {
_, err = cowbyClusterClient.Cluster(workspaces[0]).WildwestV1alpha1().Cowboys("default").Create(ctx, &cowboy, metav1.CreateOptions{})
_, err = cowbyClusterClient.Cluster(paths[0]).WildwestV1alpha1().Cowboys("default").Create(ctx, &cowboy, metav1.CreateOptions{})
if err != nil && !errors.IsAlreadyExists(err) {
return false
}
return testWebhook.Calls() >= 1
}, wait.ForeverTestTimeout, 100*time.Millisecond)
require.Equal(t, workspaces[0].Spec.Cluster, clusterInReviewObject.Load(), "expected that the object passed to the webhook has the kcp.io/cluster annotation set")

// Avoid race condition here by making sure that CRD is served after installing the types into logical clusters
t.Logf("Creating cowboy resource in second logical cluster")
require.Eventually(t, func() bool {
_, err = cowbyClusterClient.Cluster(workspaces[1]).WildwestV1alpha1().Cowboys("default").Create(ctx, &cowboy, metav1.CreateOptions{})
_, err = cowbyClusterClient.Cluster(paths[1]).WildwestV1alpha1().Cowboys("default").Create(ctx, &cowboy, metav1.CreateOptions{})
if err != nil && !errors.IsAlreadyExists(err) {
return false
}
Expand Down Expand Up @@ -183,8 +195,8 @@ func TestValidatingWebhookInWorkspace(t *testing.T) {
deserializer := codecs.UniversalDeserializer()

testWebhook := webhookserver.AdmissionWebhookServer{
Response: v1.AdmissionResponse{
Allowed: true,
ResponseFn: func(review *v1.AdmissionReview) (*v1.AdmissionResponse, error) {
return &v1.AdmissionResponse{Allowed: true}, nil
},
ObjectGVK: schema.GroupVersionKind{
Group: "wildwest.dev",
Expand Down
10 changes: 8 additions & 2 deletions test/e2e/fixtures/webhook/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import (
)

type AdmissionWebhookServer struct {
Response admissionv1.AdmissionResponse
ResponseFn func(review *admissionv1.AdmissionReview) (*admissionv1.AdmissionResponse, error)
ObjectGVK schema.GroupVersionKind
Deserializer runtime.Decoder

Expand Down Expand Up @@ -137,7 +137,13 @@ func (s *AdmissionWebhookServer) ServeHTTP(resp http.ResponseWriter, req *http.R
responseAdmissionReview := &admissionv1.AdmissionReview{
TypeMeta: requestedAdmissionReview.TypeMeta,
}
responseAdmissionReview.Response = &s.Response
r, err := s.ResponseFn(requestedAdmissionReview)
if err != nil {
s.t.Logf("%v", err)
http.Error(resp, err.Error(), http.StatusInternalServerError)
return
}
responseAdmissionReview.Response = r
responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID

respBytes, err := json.Marshal(responseAdmissionReview)
Expand Down

0 comments on commit c707d7f

Please sign in to comment.