diff --git a/hooks/subnamespace.go b/hooks/subnamespace.go index 5b0b53e..cfc6d8d 100644 --- a/hooks/subnamespace.go +++ b/hooks/subnamespace.go @@ -3,14 +3,18 @@ package hooks import ( "context" "encoding/json" + "errors" "fmt" "net/http" + "regexp" accuratev1 "github.com/cybozu-go/accurate/api/v1" + "github.com/cybozu-go/accurate/pkg/config" "github.com/cybozu-go/accurate/pkg/constants" admissionv1 "k8s.io/api/admission/v1" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/webhook" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" @@ -47,7 +51,8 @@ func (m *subNamespaceMutator) Handle(ctx context.Context, req admission.Request) type subNamespaceValidator struct { client.Client - dec *admission.Decoder + dec *admission.Decoder + namingPolicies []config.NamingPolicy } var _ admission.Handler = &subNamespaceValidator{} @@ -67,15 +72,45 @@ func (v *subNamespaceValidator) Handle(ctx context.Context, req admission.Reques return admission.Errored(http.StatusInternalServerError, err) } - if ns.Labels[constants.LabelType] == constants.NSTypeRoot || ns.Labels[constants.LabelParent] != "" { - return admission.Allowed("") + if ns.Labels[constants.LabelType] != constants.NSTypeRoot && ns.Labels[constants.LabelParent] == "" { + return admission.Denied(fmt.Sprintf("namespace %s is neither a root nor a sub namespace", ns.Name)) + } + + if err := v.MatchNamingPolicy(ctx, sn); err != nil { + return admission.Denied(fmt.Sprintf("namespace %s is not match naming policies", ns.Name)) + } + + return admission.Allowed("") +} + +func (v *subNamespaceValidator) MatchNamingPolicy(ctx context.Context, sn *accuratev1.SubNamespace) error { + logger := log.FromContext(ctx) + logger.Info("MatchNamingPolicy called\n") + for _, policy := range v.namingPolicies { + logger.Info("namingPolicies", "Root", policy.Root, "Match", policy.Match) + logger.Info("sn", "Namespace", sn.Namespace, "Name", sn.Name) + matched, err := regexp.MatchString(policy.Root, sn.Namespace) + if err != nil { + return err + } + if matched { + logger.Info("matched root namespace", "Namespace", sn.Namespace, "Name", sn.Name, "Root", policy.Root) + matched, err := regexp.MatchString(policy.Match, sn.Name) + if err != nil { + return err + } + if !matched { + logger.Info("matched sn name", "Namespace", sn.Namespace, "Name", sn.Name, "Match", policy.Match) + return errors.New("naming policy is not matched") + } + } } - return admission.Denied(fmt.Sprintf("namespace %s is neither a root nor a sub namespace", ns.Name)) + return nil } // SetupSubNamespaceWebhook registers the webhooks for SubNamespace -func SetupSubNamespaceWebhook(mgr manager.Manager, dec *admission.Decoder) { +func SetupSubNamespaceWebhook(mgr manager.Manager, dec *admission.Decoder, namingPolicies []config.NamingPolicy) { serv := mgr.GetWebhookServer() m := &subNamespaceMutator{ @@ -84,8 +119,9 @@ func SetupSubNamespaceWebhook(mgr manager.Manager, dec *admission.Decoder) { serv.Register("/mutate-accurate-cybozu-com-v1-subnamespace", &webhook.Admission{Handler: m}) v := &subNamespaceValidator{ - Client: mgr.GetClient(), - dec: dec, + Client: mgr.GetClient(), + dec: dec, + namingPolicies: namingPolicies, } serv.Register("/validate-accurate-cybozu-com-v1-subnamespace", &webhook.Admission{Handler: v}) } diff --git a/hooks/subnamespace_test.go b/hooks/subnamespace_test.go index 31f83d4..f310765 100644 --- a/hooks/subnamespace_test.go +++ b/hooks/subnamespace_test.go @@ -75,4 +75,68 @@ var _ = Describe("SubNamespace webhook", func() { Expect(controllerutil.ContainsFinalizer(sn, constants.Finalizer)).To(BeTrue()) }) + + Context("Naming Policy", func() { + When("the root namespace name is matched some Root Naming Policies", func() { + When("the SubNamespace name is matched to the Root's Match Naming Policy", func() { + It("should allow creation of SubNamespace in a root namespace - pattern1", func() { + ns := &corev1.Namespace{} + ns.Name = "naming-policy-root-1" + ns.Labels = map[string]string{constants.LabelType: constants.NSTypeRoot} + err := k8sClient.Create(ctx, ns) + Expect(err).NotTo(HaveOccurred()) + + sn := &accuratev1.SubNamespace{} + sn.Namespace = "naming-policy-root-1" + sn.Name = "naming-policy-root-1-child" + err = k8sClient.Create(ctx, sn) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should allow creation of SubNamespace in a root namespace - pattern2", func() { + ns := &corev1.Namespace{} + ns.Name = "root-ns-match-1" + ns.Labels = map[string]string{constants.LabelType: constants.NSTypeRoot} + err := k8sClient.Create(ctx, ns) + Expect(err).NotTo(HaveOccurred()) + + sn := &accuratev1.SubNamespace{} + sn.Namespace = "root-ns-match-1" + sn.Name = "child-match-1" + err = k8sClient.Create(ctx, sn) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + When("the SubNamespace name is not matched to the Root's Match Naming Policy", func() { + It("should deny creation of SubNamespace in a root namespace - pattern1", func() { + ns := &corev1.Namespace{} + ns.Name = "naming-policy-root-2" + ns.Labels = map[string]string{constants.LabelType: constants.NSTypeRoot} + err := k8sClient.Create(ctx, ns) + Expect(err).NotTo(HaveOccurred()) + + sn := &accuratev1.SubNamespace{} + sn.Namespace = "naming-policy-root-2" + sn.Name = "naming-policy-root-2--child" + err = k8sClient.Create(ctx, sn) + Expect(err).To(HaveOccurred()) + }) + + It("should deny creation of SubNamespace in a root namespace - pattern2", func() { + ns := &corev1.Namespace{} + ns.Name = "root-ns-match-2" + ns.Labels = map[string]string{constants.LabelType: constants.NSTypeRoot} + err := k8sClient.Create(ctx, ns) + Expect(err).NotTo(HaveOccurred()) + + sn := &accuratev1.SubNamespace{} + sn.Namespace = "root-ns-match-2" + sn.Name = "child-2" + err = k8sClient.Create(ctx, sn) + Expect(err).To(HaveOccurred()) + }) + }) + }) + }) }) diff --git a/hooks/suite_test.go b/hooks/suite_test.go index 43caa21..babcdfd 100644 --- a/hooks/suite_test.go +++ b/hooks/suite_test.go @@ -15,6 +15,7 @@ import ( admissionv1beta1 "k8s.io/api/admission/v1beta1" //+kubebuilder:scaffold:imports accuratev1 "github.com/cybozu-go/accurate/api/v1" + "github.com/cybozu-go/accurate/pkg/config" "github.com/cybozu-go/accurate/pkg/indexing" "k8s.io/apimachinery/pkg/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" @@ -91,7 +92,20 @@ var _ = BeforeSuite(func() { dec, err := admission.NewDecoder(scheme) Expect(err).NotTo(HaveOccurred()) SetupNamespaceWebhook(mgr, dec) - SetupSubNamespaceWebhook(mgr, dec) + SetupSubNamespaceWebhook(mgr, dec, []config.NamingPolicy{ + { + Root: "naming-policy-root-1", + Match: "naming-policy-root-1-child", + }, + { + Root: "naming-policy-root-2", + Match: "naming-policy-root-2-child", + }, + { + Root: ".+-match-.+", + Match: ".+-match-.+", + }, + }) go func() { err = mgr.Start(ctx) diff --git a/pkg/config/types.go b/pkg/config/types.go index aba5b19..24238ba 100644 --- a/pkg/config/types.go +++ b/pkg/config/types.go @@ -10,11 +10,17 @@ import ( "sigs.k8s.io/yaml" ) +type NamingPolicy struct { + Root string `json:"root,omitempty"` + Match string `json:"match,omitempty"` +} + // Config represents the configuration file of Accurate. type Config struct { LabelKeys []string `json:"labelKeys,omitempty"` AnnotationKeys []string `json:"annotationKeys,omitempty"` Watches []metav1.GroupVersionKind `json:"watches,omitempty"` + NamingPolicies []NamingPolicy `json:"namingPolicies,omitempty"` } // Validate validates the configurations.