Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ReplicaSet validation webhook #341

Merged
merged 10 commits into from
Nov 11, 2022
10 changes: 7 additions & 3 deletions pkg/utils/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,10 +242,14 @@ func ShouldPropagateObj(informerManager informer.Manager, uObj *unstructured.Uns
return true, nil
}

// ShouldPropagateNamespace decides if we should propagate the resources in the namespace
// IsReservedNamespace indicates if an argued namespace is reserved.
func IsReservedNamespace(namespace string) bool {
return strings.HasPrefix(namespace, fleetPrefix) || strings.HasPrefix(namespace, kubePrefix)
}

// ShouldPropagateNamespace decides if we should propagate the resources in the namespace.
func ShouldPropagateNamespace(namespace string, skippedNamespaces map[string]bool) bool {
// special case for namespace have the reserved prefix
if strings.HasPrefix(namespace, fleetPrefix) || strings.HasPrefix(namespace, kubePrefix) {
if IsReservedNamespace(namespace) {
return false
}

Expand Down
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)
}
4 changes: 3 additions & 1 deletion pkg/webhook/pod/pod_validating_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

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

const (
Expand Down Expand Up @@ -43,7 +45,7 @@ func (v *podValidator) Handle(ctx context.Context, req admission.Request) admiss
if err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
if pod.Namespace != "kube-system" && pod.Namespace != "fleet-system" {
if !utils.IsReservedNamespace(pod.Namespace) {
return admission.Denied(fmt.Sprintf("Pod %s/%s creation is disallowed in the fleet hub cluster", pod.Namespace, pod.Name))
}
}
Expand Down
58 changes: 58 additions & 0 deletions pkg/webhook/replicaset/replicaset_validating_webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
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"

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

const (
// ValidationPath is the webhook service path which admission requests are routed to for validating ReplicaSet resources.
ValidationPath = "/validate-apps-v1-replicaset"
)

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

// Add registers the webhook for K8s bulit-in object types.
func Add(mgr manager.Manager) error {
Arvindthiru marked this conversation as resolved.
Show resolved Hide resolved
hookServer := mgr.GetWebhookServer()
hookServer.Register(ValidationPath, &webhook.Admission{Handler: &replicaSetValidator{Client: mgr.GetClient()}})
return nil
}

// 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{}
if err := v.decoder.Decode(req, rs); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
if !utils.IsReservedNamespace(rs.Namespace) {
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 {
Arvindthiru marked this conversation as resolved.
Show resolved Hide resolved
v.decoder = d
return nil
}
34 changes: 30 additions & 4 deletions pkg/webhook/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"time"

admv1 "k8s.io/api/admissionregistration/v1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand All @@ -32,6 +33,7 @@ import (
"go.goms.io/fleet/cmd/hubagent/options"
"go.goms.io/fleet/pkg/webhook/clusterresourceplacement"
"go.goms.io/fleet/pkg/webhook/pod"
"go.goms.io/fleet/pkg/webhook/replicaset"
)

const (
Expand Down Expand Up @@ -82,7 +84,7 @@ func NewWebhookConfig(mgr manager.Manager, port int, clientConnectionType *optio
}
caPEM, err := w.genCertificate(certDir)
if err != nil {
return nil, err // TODO
Ealianis marked this conversation as resolved.
Show resolved Hide resolved
return nil, err
}
w.caPEM = caPEM
return &w, err
Expand All @@ -97,9 +99,9 @@ func (w *Config) Start(ctx context.Context) error {
return nil
}

// createFleetWebhookConfiguration creates the ValidatingWebhookConfiguration object for the webhook
// createFleetWebhookConfiguration creates the ValidatingWebhookConfiguration object for the webhook.
func (w *Config) createFleetWebhookConfiguration(ctx context.Context) error {
failPolicy := admv1.Fail // reject request if the webhook doesn't work
failPolicy := admv1.Fail
sideEffortsNone := admv1.SideEffectClassNone
namespacedScope := admv1.NamespacedScope
clusterScope := admv1.ClusterScope
Expand Down Expand Up @@ -155,6 +157,26 @@ func (w *Config) createFleetWebhookConfiguration(ctx context.Context) error {
},
},
},
{
Name: "fleet.replicaset.validating",
ClientConfig: w.createClientConfig(appsv1.ReplicaSet{}),
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{"replicasets"},
Scope: &namespacedScope,
},
},
},
},
},
}

Expand All @@ -178,6 +200,7 @@ func (w *Config) createFleetWebhookConfiguration(ctx context.Context) error {
return nil
}

// createClientConfig generates the client configuration with either service ref or URL for the argued interface.
func (w *Config) createClientConfig(webhookInterface interface{}) admv1.WebhookClientConfig {
serviceRef := admv1.ServiceReference{
Namespace: w.serviceNamespace,
Expand All @@ -192,6 +215,9 @@ func (w *Config) createClientConfig(webhookInterface interface{}) admv1.WebhookC
case fleetv1alpha1.ClusterResourcePlacement:
serviceEndpoint = w.serviceURL + clusterresourceplacement.ValidationPath
serviceRef.Path = pointer.String(clusterresourceplacement.ValidationPath)
case appsv1.ReplicaSet:
serviceEndpoint = w.serviceURL + replicaset.ValidationPath
serviceRef.Path = pointer.String(replicaset.ValidationPath)
}

config := admv1.WebhookClientConfig{
Expand All @@ -206,7 +232,7 @@ func (w *Config) createClientConfig(webhookInterface interface{}) admv1.WebhookC
return config
}

// genCertificate generates the serving cerficiate for the webhook server
// genCertificate generates the serving cerficiate for the webhook server.
func (w *Config) genCertificate(certDir string) ([]byte, error) {
caPEM, certPEM, keyPEM, err := w.genSelfSignedCert()
if err != nil {
Expand Down
22 changes: 18 additions & 4 deletions test/e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,31 @@ var (
imc *v1alpha1.InternalMemberCluster
ctx context.Context

// This namespace will store Member cluster-related CRs, such as v1alpha1.MemberCluster
// The fleet-system namespace.
fleetSystemNamespace = &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("fleet-system"),
},
}

// The kube-system namespace
kubeSystemNamespace = &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("kube-system"),
},
}

// This namespace will store Member cluster-related CRs, such as v1alpha1.MemberCluster.
memberNamespace = &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf(utils.NamespaceNameFormat, MemberCluster.ClusterName),
Name: fmt.Sprintf("fleet-member-%s", MemberCluster.ClusterName),
},
}

// This namespace in HubCluster will store v1alpha1.Work to simulate Work-related features in Hub Cluster.
workNamespace = &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf(utils.NamespaceNameFormat, MemberCluster.ClusterName),
Name: fmt.Sprintf("fleet-member-%s", MemberCluster.ClusterName),
},
}

Expand Down Expand Up @@ -174,7 +188,7 @@ var _ = BeforeSuite(func() {
identity := rbacv1.Subject{
Name: "member-agent-sa",
Kind: "ServiceAccount",
Namespace: "fleet-system",
Namespace: utils.FleetSystemNamespace,
Ealianis marked this conversation as resolved.
Show resolved Hide resolved
}
mc = &v1alpha1.MemberCluster{
ObjectMeta: metav1.ObjectMeta{
Expand Down
Loading