From 606318c8a72620cb724a0e916f40f6483085d241 Mon Sep 17 00:00:00 2001 From: bells17 Date: Thu, 11 Nov 2021 18:30:06 +0900 Subject: [PATCH] Support Naming Policy (#17) --- charts/accurate/Chart.yaml | 2 +- charts/accurate/values.yaml | 19 ++++ cmd/accurate-controller/sub/run.go | 4 +- config.yaml | 27 ----- docs/config.md | 60 +++++++++- hooks/subnamespace.go | 67 +++++++++-- hooks/subnamespace_test.go | 174 +++++++++++++++++++++++++++++ hooks/suite_test.go | 39 ++++++- pkg/config/testdata/config.yaml | 6 + pkg/config/testdata/invalid.yaml | 6 + pkg/config/types.go | 28 ++++- pkg/config/types_test.go | 85 ++++++++++++++ 12 files changed, 476 insertions(+), 41 deletions(-) delete mode 100644 config.yaml diff --git a/charts/accurate/Chart.yaml b/charts/accurate/Chart.yaml index 96b5e5f..9395331 100644 --- a/charts/accurate/Chart.yaml +++ b/charts/accurate/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.2.0 +version: 0.2.1 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to diff --git a/charts/accurate/values.yaml b/charts/accurate/values.yaml index f27ade9..7f49797 100644 --- a/charts/accurate/values.yaml +++ b/charts/accurate/values.yaml @@ -54,6 +54,25 @@ controller: - version: v1 kind: ResourceQuota + # controller.config.namingPolicies -- List of nameing policy for SubNamespaces. + # root and match are both regular expressions. + # When a SubNamespace is created in a tree starting from a root namespace and the root namespace's name matches the "root" regular expression, the SubNamespace name is validated with the "match" regular expression. + # + # "match" namingPolicies can use variables of regexp capture group naming of "root" namingPolicies. + # example: + # root: ^app-(?P.*) + # match: ^app-${team}-.* + # root namespace: app-team1 + # compiled match naming policy: ^app-team1-.* + # This feature is provided using https://pkg.go.dev/regexp#Regexp.Expand + # namingPolicies: + # - root: foo + # match: foo_.* + # - root: bar + # match: bar_.* + # - root: ^app-(?P.*) + # match: ^app-${team}-.* + additionalRBAC: # controller.additionalRBAC.rules -- Specify the RBAC rules to be added to the controller. # ClusterRole and ClusterRoleBinding are created with the names `{{ release name }}-additional-resources`. diff --git a/cmd/accurate-controller/sub/run.go b/cmd/accurate-controller/sub/run.go index 373f7cf..9168045 100644 --- a/cmd/accurate-controller/sub/run.go +++ b/cmd/accurate-controller/sub/run.go @@ -102,7 +102,9 @@ func subMain(ns, addr string, port int) error { }).SetupWithManager(mgr); err != nil { return fmt.Errorf("unable to create SubNamespace controller: %w", err) } - hooks.SetupSubNamespaceWebhook(mgr, dec) + if err = hooks.SetupSubNamespaceWebhook(mgr, dec, cfg.NamingPolicyRegexps); err != nil { + return fmt.Errorf("unable to create SubNamespace webhook: %w", err) + } // Resource propagation controller for _, res := range watched { diff --git a/config.yaml b/config.yaml deleted file mode 100644 index ce83fca..0000000 --- a/config.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# Labels to be propagated to sub-namespaces. -# It is also possible to specify a glob pattern that can be interpreted by Go's "path.Match" func. -# https://pkg.go.dev/path#Match -labelKeys: -- team - -# Annotations to be propagated to sub-namespaces. -# It is also possible to specify a glob pattern that can be interpreted by Go's "path.Match" func. -# https://pkg.go.dev/path#Match -annotationKeys: -# An example to propagate an annotation for MetalLB -# https://metallb.universe.tf/usage/#requesting-specific-ips -- metallb.universe.tf/address-pool - -# List of GVK for namespace-scoped resources that can be propagated. -# Any namespace-scoped resource is allowed. -watches: -- group: rbac.authorization.k8s.io - version: v1 - kind: Role -- group: rbac.authorization.k8s.io - version: v1 - kind: RoleBinding -- version: v1 - kind: Secret -- version: v1 - kind: ResourceQuota diff --git a/docs/config.md b/docs/config.md index aef0c19..2ed495d 100644 --- a/docs/config.md +++ b/docs/config.md @@ -11,7 +11,46 @@ Read [Helm Chart](helm.md) for details. The repository includes an example as follows: ```yaml -{{#include ../config.yaml}} +# Labels to be propagated to sub-namespaces. +# It is also possible to specify a glob pattern that can be interpreted by Go's "path.Match" func. +# https://pkg.go.dev/path#Match +labelKeys: +- team + +# Annotations to be propagated to sub-namespaces. +# It is also possible to specify a glob pattern that can be interpreted by Go's "path.Match" func. +# https://pkg.go.dev/path#Match +annotationKeys: +# An example to propagate an annotation for MetalLB +# https://metallb.universe.tf/usage/#requesting-specific-ips +- metallb.universe.tf/address-pool + +# List of GVK for namespace-scoped resources that can be propagated. +# Any namespace-scoped resource is allowed. +watches: +- group: rbac.authorization.k8s.io + version: v1 + kind: Role +- group: rbac.authorization.k8s.io + version: v1 + kind: RoleBinding +- version: v1 + kind: Secret +- version: v1 + kind: ResourceQuota + +# List of nameing policy for SubNamespaces. +# root and match are both regular expressions. +# When a SubNamespace is created in a tree starting from a root namespace and the root namespace's name matches the "root" regular expression, the SubNamespace name is validated with the "match" regular expression. +# +# "match" namingPolicies can use variables of regexp capture group naming of "root" namingPolicies. +# example: +# root: ^app-(?P.*) +# match: ^app-${team}-.* +# root namespace: app-team1 +# compiled match naming policy: ^app-team1-.* +# This feature is provided using https://pkg.go.dev/regexp#Regexp.Expand +namingPolicies: [] ``` Only labels and annotations specified in the configuration file will be inherited. @@ -49,6 +88,25 @@ controller: kind: RoleBinding - version: v1 kind: Secret + + # controller.config.namingPolicies -- List of nameing policy for SubNamespaces. + # root and match are both regular expressions. + # When a SubNamespace is created in a tree starting from a root namespace and the root namespace's name matches the "root" regular expression, the SubNamespace name is validated with the "match" regular expression. + # + # "match" namingPolicies can use variables of regexp capture group naming of "root" namingPolicies. + # example: + # root: ^app-(?P.*) + # match: ^app-${team}-.* + # root namespace: app-team1 + # compiled match naming policy: ^app-team1-.* + # This feature is provided using https://pkg.go.dev/regexp#Regexp.Expand + namingPolicies: + - root: foo + match: foo_.* + - root: bar + match: bar_.* + - root: ^app-(?P.*) + match: ^app-${team}-.* ``` diff --git a/hooks/subnamespace.go b/hooks/subnamespace.go index 5b0b53e..405842b 100644 --- a/hooks/subnamespace.go +++ b/hooks/subnamespace.go @@ -5,11 +5,14 @@ import ( "encoding/json" "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" + apierrors "k8s.io/apimachinery/pkg/api/errors" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/webhook" @@ -47,7 +50,8 @@ func (m *subNamespaceMutator) Handle(ctx context.Context, req admission.Request) type subNamespaceValidator struct { client.Client - dec *admission.Decoder + dec *admission.Decoder + namingPolicies []config.NamingPolicyRegexp } var _ admission.Handler = &subNamespaceValidator{} @@ -67,15 +71,62 @@ 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)) + } + + root, err := v.getRootNamespace(ctx, ns) + if err != nil { + return admission.Denied(err.Error()) + } + ok, msg, err := v.notMatchingNamingPolicy(ctx, sn.Name, root.Name) + if err != nil { + return admission.Errored(http.StatusInternalServerError, err) + } + if !ok { + return admission.Denied(fmt.Sprintf("namespace %s is not match naming policies: %s", ns.Name, msg)) + } + return admission.Allowed("") +} + +func (v *subNamespaceValidator) getRootNamespace(ctx context.Context, ns *corev1.Namespace) (*corev1.Namespace, error) { + if ns.Labels[constants.LabelType] == constants.NSTypeRoot { + return ns, nil } - return admission.Denied(fmt.Sprintf("namespace %s is neither a root nor a sub namespace", ns.Name)) + parent := &corev1.Namespace{} + if err := v.Get(ctx, client.ObjectKey{Name: ns.Labels[constants.LabelParent]}, parent); err != nil { + if !apierrors.IsNotFound(err) { + return nil, fmt.Errorf("failed to get namespace %s: %w", ns.Labels[constants.LabelParent], err) + } + return nil, fmt.Errorf("namespace %s is not found", ns.Labels[constants.LabelParent]) + } + return v.getRootNamespace(ctx, parent) +} + +func (v *subNamespaceValidator) notMatchingNamingPolicy(ctx context.Context, ns, root string) (bool, string, error) { + for _, policy := range v.namingPolicies { + matches := policy.Root.FindAllStringSubmatchIndex(root, -1) + if len(matches) > 0 { + m := []byte{} + for _, match := range matches { + m = policy.Root.ExpandString(m, policy.Match, root, match) + } + r, err := regexp.Compile(string(m)) + if err != nil { + return false, "", fmt.Errorf("invalid naming policy: %w", err) + } + + if !r.MatchString(ns) { + return false, fmt.Sprintf("namespace - target=%s root=%s denied policy - root=%s match=%s", ns, root, policy.Root, policy.Match), nil + } + } + } + return true, "", nil } // SetupSubNamespaceWebhook registers the webhooks for SubNamespace -func SetupSubNamespaceWebhook(mgr manager.Manager, dec *admission.Decoder) { +func SetupSubNamespaceWebhook(mgr manager.Manager, dec *admission.Decoder, namingPolicyRegexps []config.NamingPolicyRegexp) error { serv := mgr.GetWebhookServer() m := &subNamespaceMutator{ @@ -84,8 +135,10 @@ 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: namingPolicyRegexps, } serv.Register("/validate-accurate-cybozu-com-v1-subnamespace", &webhook.Admission{Handler: v}) + return nil } diff --git a/hooks/subnamespace_test.go b/hooks/subnamespace_test.go index 31f83d4..ee8a565 100644 --- a/hooks/subnamespace_test.go +++ b/hooks/subnamespace_test.go @@ -75,4 +75,178 @@ 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()) + }) + + It("should allow creation of SubNamespace in a root namespace - pattern3", func() { + root := &corev1.Namespace{} + root.Name = "ns-root-1" + root.Labels = map[string]string{constants.LabelType: constants.NSTypeRoot} + err := k8sClient.Create(ctx, root) + Expect(err).NotTo(HaveOccurred()) + + parent := &corev1.Namespace{} + parent.Name = "ns-root-1-parent" + parent.Labels = map[string]string{constants.LabelParent: "ns-root-1"} + err = k8sClient.Create(ctx, parent) + Expect(err).NotTo(HaveOccurred()) + + sn := &accuratev1.SubNamespace{} + sn.Namespace = "ns-root-1-parent" + sn.Name = "ns-root-1-parent-child" + err = k8sClient.Create(ctx, sn) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should allow creation of SubNamespace in a root namespace - pattern4", func() { + root := &corev1.Namespace{} + root.Name = "app-team1" + root.Labels = map[string]string{constants.LabelType: constants.NSTypeRoot} + err := k8sClient.Create(ctx, root) + Expect(err).NotTo(HaveOccurred()) + + sn := &accuratev1.SubNamespace{} + sn.Namespace = "app-team1" + sn.Name = "app-team1-child" + err = k8sClient.Create(ctx, sn) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should allow creation of SubNamespace in a root namespace - pattern5", func() { + root := &corev1.Namespace{} + root.Name = "app-team2-app1" + root.Labels = map[string]string{constants.LabelType: constants.NSTypeRoot} + err := k8sClient.Create(ctx, root) + Expect(err).NotTo(HaveOccurred()) + + sn := &accuratev1.SubNamespace{} + sn.Namespace = "app-team2-app1" + sn.Name = "app-team2-app1-subapp1" + err = k8sClient.Create(ctx, sn) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should allow creation of SubNamespace in a root namespace - pattern6", func() { + root := &corev1.Namespace{} + root.Name = "unuse-naming-group-team1" + root.Labels = map[string]string{constants.LabelType: constants.NSTypeRoot} + err := k8sClient.Create(ctx, root) + Expect(err).NotTo(HaveOccurred()) + + sn := &accuratev1.SubNamespace{} + sn.Namespace = "unuse-naming-group-team1" + sn.Name = "unuse-naming-group-child1" + 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()) + }) + + It("should deny creation of SubNamespace in a root namespace - pattern3", func() { + root := &corev1.Namespace{} + root.Name = "ns-root-2" + root.Labels = map[string]string{constants.LabelType: constants.NSTypeRoot} + err := k8sClient.Create(ctx, root) + Expect(err).NotTo(HaveOccurred()) + + parent := &corev1.Namespace{} + parent.Name = "ns-root-2-parent" + parent.Labels = map[string]string{constants.LabelParent: "ns-root-1"} + err = k8sClient.Create(ctx, parent) + Expect(err).NotTo(HaveOccurred()) + + sn := &accuratev1.SubNamespace{} + sn.Namespace = "ns-root-2-parent" + sn.Name = "not-ns-root-2-parent-child" + err = k8sClient.Create(ctx, sn) + Expect(err).To(HaveOccurred()) + }) + + It("should deny creation of SubNamespace in a root namespace - pattern4", func() { + root := &corev1.Namespace{} + root.Name = "app-team10" + root.Labels = map[string]string{constants.LabelType: constants.NSTypeRoot} + err := k8sClient.Create(ctx, root) + Expect(err).NotTo(HaveOccurred()) + + sn := &accuratev1.SubNamespace{} + sn.Namespace = "app-team10" + sn.Name = "app-team20-child" + err = k8sClient.Create(ctx, sn) + Expect(err).To(HaveOccurred()) + }) + + It("should deny creation of SubNamespace in a root namespace - pattern5", func() { + root := &corev1.Namespace{} + root.Name = "unuse-naming-group-team2" + root.Labels = map[string]string{constants.LabelType: constants.NSTypeRoot} + err := k8sClient.Create(ctx, root) + Expect(err).NotTo(HaveOccurred()) + + sn := &accuratev1.SubNamespace{} + sn.Namespace = "unuse-naming-group-team2" + sn.Name = "unuse-naming-group-team2-foo" + err = k8sClient.Create(ctx, sn) + Expect(err).To(HaveOccurred()) + }) + }) + }) + }) }) diff --git a/hooks/suite_test.go b/hooks/suite_test.go index 43caa21..518c9f4 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,43 @@ var _ = BeforeSuite(func() { dec, err := admission.NewDecoder(scheme) Expect(err).NotTo(HaveOccurred()) SetupNamespaceWebhook(mgr, dec) - SetupSubNamespaceWebhook(mgr, dec) + + conf := config.Config{ + NamingPolicies: []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-.+", + }, + { + Root: "^ns-root.+", + Match: "^ns-root.+", + }, + { + Root: "^app-(?P.*)", + Match: "^app-${team}-.*", + }, + { + Root: "^app-(?P[^-]*)-(?P[^-]*)", + Match: "^app-$team-$app-.*", + }, + { + Root: "^unuse-naming-group-(?P.*)", + Match: "^unuse-naming-group-child1", + }, + }, + } + err = conf.Validate(mgr.GetRESTMapper()) + Expect(err).NotTo(HaveOccurred()) + err = SetupSubNamespaceWebhook(mgr, dec, conf.NamingPolicyRegexps) + Expect(err).NotTo(HaveOccurred()) go func() { err = mgr.Start(ctx) diff --git a/pkg/config/testdata/config.yaml b/pkg/config/testdata/config.yaml index 32192c9..b2a0ad3 100644 --- a/pkg/config/testdata/config.yaml +++ b/pkg/config/testdata/config.yaml @@ -12,3 +12,9 @@ watches: kind: Deployment - version: v1 kind: Secret + +namingPolicies: +- root: foo + match: bar +- root: a + match: b diff --git a/pkg/config/testdata/invalid.yaml b/pkg/config/testdata/invalid.yaml index a65792d..f4d5ea3 100644 --- a/pkg/config/testdata/invalid.yaml +++ b/pkg/config/testdata/invalid.yaml @@ -13,4 +13,10 @@ watches: - version: v1 kind: Secret +namingPolicies: +- root: foo + match: bar +- root: a + match: b + invalid: data diff --git a/pkg/config/types.go b/pkg/config/types.go index aba5b19..4c073ae 100644 --- a/pkg/config/types.go +++ b/pkg/config/types.go @@ -3,6 +3,7 @@ package config import ( "fmt" "path" + "regexp" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -10,11 +11,24 @@ import ( "sigs.k8s.io/yaml" ) +// NamingPolicy represents naming policies for Namespaces created from SubNamespaces. +type NamingPolicy struct { + Root string `json:"root"` + Match string `json:"match"` +} + +type NamingPolicyRegexp struct { + Root *regexp.Regexp + Match string +} + // 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"` + LabelKeys []string `json:"labelKeys,omitempty"` + AnnotationKeys []string `json:"annotationKeys,omitempty"` + Watches []metav1.GroupVersionKind `json:"watches,omitempty"` + NamingPolicies []NamingPolicy `json:"namingPolicies,omitempty"` + NamingPolicyRegexps []NamingPolicyRegexp } // Validate validates the configurations. @@ -43,6 +57,14 @@ func (c *Config) Validate(mapper meta.RESTMapper) error { return fmt.Errorf("%s is not namespace-scoped", gvk.String()) } } + + for _, policy := range c.NamingPolicies { + root, err := regexp.Compile(policy.Root) + if err != nil { + return fmt.Errorf("invalid naming policy: %w", err) + } + c.NamingPolicyRegexps = append(c.NamingPolicyRegexps, NamingPolicyRegexp{Root: root, Match: policy.Match}) + } return nil } diff --git a/pkg/config/types_test.go b/pkg/config/types_test.go index e91c0f5..247e254 100644 --- a/pkg/config/types_test.go +++ b/pkg/config/types_test.go @@ -7,7 +7,12 @@ import ( "github.com/google/go-cmp/cmp" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/discovery/cached/memory" + fakediscovery "k8s.io/client-go/discovery/fake" + fakeclientset "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/restmapper" ) var _ = Describe("Validate", func() { @@ -81,6 +86,10 @@ func TestLoad(t *testing.T) { t.Error("wrong kind:", gvk.Kind) } + if len(c.NamingPolicies) != 2 { + t.Error("wrong number of namingPolicies:", len(c.NamingPolicies)) + } + c = &Config{} err = c.Load(invalidData) if err == nil { @@ -88,3 +97,79 @@ func TestLoad(t *testing.T) { } t.Log(err) } + +func TestValidate(t *testing.T) { + m := newFakeRESTMapper() + testcases := []struct { + config *Config + isValid bool + }{ + { + config: &Config{ + LabelKeys: []string{"a", "b"}, + AnnotationKeys: []string{"foo", "bar"}, + Watches: []metav1.GroupVersionKind{ + { + Group: "", + Version: "v1", + Kind: "Secret", + }, + { + Group: "apps", + Version: "v1", + Kind: "Deployment", + }, + }, + NamingPolicies: []NamingPolicy{ + { + Root: "foo", + Match: "bar", + }, + { + Root: "a", + Match: "b", + }, + }, + }, + isValid: true, + }, + { + config: &Config{ + NamingPolicies: []NamingPolicy{ + { + Root: "(", + Match: "abc", + }, + }, + }, + isValid: false, + }, + } + + for _, testcase := range testcases { + err := testcase.config.Validate(m) + if testcase.isValid && err != nil { + t.Fatal(err) + } + if !testcase.isValid && err == nil { + t.Fatal("invalid data are validated successfully") + } + } +} + +func newFakeRESTMapper() meta.RESTMapper { + cs := &fakeclientset.Clientset{} + cs.Fake.Resources = append(cs.Fake.Resources, &metav1.APIResourceList{ + GroupVersion: "v1", + APIResources: []metav1.APIResource{ + {Name: "secrets", Namespaced: true, Kind: "Secret"}, + }, + }, &metav1.APIResourceList{ + GroupVersion: "apps/v1", + APIResources: []metav1.APIResource{ + {Name: "deployments", Namespaced: true, Kind: "Deployment"}, + }, + }) + fakeDiscovery := &fakediscovery.FakeDiscovery{Fake: &cs.Fake} + return restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(fakeDiscovery)) +}