Skip to content
14 changes: 14 additions & 0 deletions pkg/webhook/add_replicaset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
Copyright (c) Microsoft Corporation.
Licensed under the MIT license.
*/

package webhook

import (
"go.goms.io/fleet/pkg/webhook/replicaset"
)

func init() {
AddToManagerFuncs = append(AddToManagerFuncs, replicaset.Add)
}
50 changes: 50 additions & 0 deletions pkg/webhook/replicaset/replicaset_validating_webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
Copyright (c) Microsoft Corporation.
Licensed under the MIT license.
*/

package replicaset

import (
"context"
"fmt"
"net/http"

admissionv1 "k8s.io/api/admission/v1"
v1 "k8s.io/api/apps/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

// Add registers the webhook for K8s bulit-in object types.
func Add(mgr manager.Manager) error {
hookServer := mgr.GetWebhookServer()
hookServer.Register("/validate-apps-v1-replicaset", &webhook.Admission{Handler: &replicaSetValidator{Client: mgr.GetClient()}})
return nil
}

type replicaSetValidator struct {
Client client.Client
decoder *admission.Decoder
}

// Handle replicaSetValidator denies all creation requests.
func (v *replicaSetValidator) Handle(ctx context.Context, req admission.Request) admission.Response {
if req.Operation == admissionv1.Create {
rs := &v1.ReplicaSet{}
err := v.decoder.Decode(req, rs)
if err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
return admission.Denied(fmt.Sprintf("ReplicaSet %s/%s creation is disallowed in the fleet hub cluster", rs.Namespace, rs.Name))
}
return admission.Allowed("")
}

// InjectDecoder injects the decoder.
func (v *replicaSetValidator) InjectDecoder(d *admission.Decoder) error {
v.decoder = d
return nil
}
48 changes: 37 additions & 11 deletions pkg/webhook/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,10 @@ func CreateFleetWebhookConfiguration(ctx context.Context, client client.Client,
sideEffortsNone := admv1.SideEffectClassNone

// We assume a headless service named fleetwebhook has been created in the pod namespace (e.g., via helm chart)
podWebhookURL := fmt.Sprintf("https://fleetwebhook.%s.svc.cluster.local:%d/validate-v1-pod", WebhookServiceNs, port)
crpWebhookURL := fmt.Sprintf("https://fleetwebhook.%s.svc.cluster.local:%d/validate-fleet-azure-com-v1alpha1-clusterresourceplacement", WebhookServiceNs, port)
replicaSetWebhookURL := fmt.Sprintf("https://fleetwebhook.%s.svc.cluster.local:%d/validate-v1-replicaset", WebhookServiceNs, port)
podWebhookURL := fmt.Sprintf("https://fleetwebhook.%s.svc.cluster.local:%d/validate-v1-pod", WebhookServiceNs, port)

namespacedScope := admv1.NamespacedScope
clusterScope := admv1.ClusterScope
whCfg := admv1.ValidatingWebhookConfiguration{
Expand All @@ -76,9 +78,9 @@ func CreateFleetWebhookConfiguration(ctx context.Context, client client.Client,
},
Webhooks: []admv1.ValidatingWebhook{
{
Name: "fleet.pod.validating",
Name: "fleet.clusterresourceplacement.validating",
ClientConfig: admv1.WebhookClientConfig{
URL: &podWebhookURL,
URL: &crpWebhookURL,
CABundle: caPEM,
},
FailurePolicy: &failPolicy,
Expand All @@ -91,18 +93,42 @@ func CreateFleetWebhookConfiguration(ctx context.Context, client client.Client,
admv1.OperationAll,
},
Rule: admv1.Rule{
APIGroups: []string{""},
APIGroups: []string{"fleet.azure.com"},
APIVersions: []string{"v1alpha1"},
Resources: []string{fleetv1alpha1.ClusterResourcePlacementResource},
Scope: &clusterScope,
},
},
},
},
{
Name: "fleet.replicaset.validating",
ClientConfig: admv1.WebhookClientConfig{
URL: &replicaSetWebhookURL,
CABundle: caPEM,
},
FailurePolicy: &failPolicy,
SideEffects: &sideEffortsNone,
AdmissionReviewVersions: []string{"v1", "v1beta1"},

Rules: []admv1.RuleWithOperations{
{
Operations: []admv1.OperationType{
admv1.Create,
},
Rule: admv1.Rule{
APIGroups: []string{"apps"},
APIVersions: []string{"v1"},
Resources: []string{"pods"},
Resources: []string{"replicaset"},
Scope: &namespacedScope,
},
},
},
},
{
Name: "fleet.clusterresourceplacement.validating",
Name: "fleet.pod.validating",
ClientConfig: admv1.WebhookClientConfig{
URL: &crpWebhookURL,
URL: &podWebhookURL,
CABundle: caPEM,
},
FailurePolicy: &failPolicy,
Expand All @@ -115,10 +141,10 @@ func CreateFleetWebhookConfiguration(ctx context.Context, client client.Client,
admv1.OperationAll,
},
Rule: admv1.Rule{
APIGroups: []string{"fleet.azure.com"},
APIVersions: []string{"v1alpha1"},
Resources: []string{fleetv1alpha1.ClusterResourcePlacementResource},
Scope: &clusterScope,
APIGroups: []string{""},
APIVersions: []string{"v1"},
Resources: []string{"pods"},
Scope: &namespacedScope,
},
},
},
Expand Down
74 changes: 74 additions & 0 deletions test/e2e/webhook_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
Copyright (c) Microsoft Corporation.
Licensed under the MIT license.
*/

package e2e

import (
"errors"
"fmt"
"reflect"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
v1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/pointer"

"go.goms.io/fleet/pkg/utils"
)

const (
kubeSystemNs = "kube-system"
fleetSystemNs = "fleet-system"
)

var (
whitelistedNamespaces = []corev1.Namespace{
{ObjectMeta: metav1.ObjectMeta{Name: kubeSystemNs}},
{ObjectMeta: metav1.ObjectMeta{Name: fleetSystemNs}},
}
)

var _ = Describe("Fleet's Hub cluster webhook tests", func() {
Context("ReplicaSet validation webhook", func() {
It("should deny create operation on ReplicaSets", func() {
rs := &v1.ReplicaSet{
TypeMeta: metav1.TypeMeta{
Kind: "ReplicaSet",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: utils.RandStr(),
Namespace: utils.RandStr(),
},
Spec: v1.ReplicaSetSpec{
Replicas: pointer.Int32(2),
MinReadySeconds: 30,
Selector: &metav1.LabelSelector{
MatchLabels: nil,
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: utils.RandStr(),
Operator: metav1.LabelSelectorOpIn,
Values: []string{utils.RandStr()},
},
},
},
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{},
},
},
}

By("attempting to create a ReplicaSet")
err := HubCluster.KubeClient.Create(ctx, rs)
var statusErr *k8sErrors.StatusError
Expect(errors.As(err, &statusErr)).To(BeTrue(), fmt.Sprintf("Create ReplicaSet call produced error %s. Error type wanted is %s.", reflect.TypeOf(err), reflect.TypeOf(&k8sErrors.StatusError{})))
Expect(statusErr.ErrStatus.Message).Should(MatchRegexp(`admission webhook "fleet.replicaset.validating" denied the request`))
})
})
})