diff --git a/apis/cluster/v1/membercluster_types.go b/apis/cluster/v1/membercluster_types.go index 2bb600115..c560933ea 100644 --- a/apis/cluster/v1/membercluster_types.go +++ b/apis/cluster/v1/membercluster_types.go @@ -25,6 +25,7 @@ import ( // +kubebuilder:printcolumn:JSONPath=`.status.resourceUsage.allocatable.memory`,name="Allocatable-Memory", priority=1, type=string // MemberCluster is a resource created in the hub cluster to represent a member cluster within a fleet. +// +kubebuilder:validation:XValidation:rule="size(self.metadata.name) < 64",message="metadata.name max length is 63" type MemberCluster struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` diff --git a/apis/cluster/v1beta1/membercluster_types.go b/apis/cluster/v1beta1/membercluster_types.go index 574e63f03..28c53dce1 100644 --- a/apis/cluster/v1beta1/membercluster_types.go +++ b/apis/cluster/v1beta1/membercluster_types.go @@ -26,6 +26,7 @@ import ( // +kubebuilder:printcolumn:JSONPath=`.status.resourceUsage.allocatable.memory`,name="Allocatable-Memory", priority=1, type=string // MemberCluster is a resource created in the hub cluster to represent a member cluster within a fleet. +// +kubebuilder:validation:XValidation:rule="size(self.metadata.name) < 64",message="metadata.name max length is 63" type MemberCluster struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` diff --git a/config/crd/bases/cluster.kubernetes-fleet.io_memberclusters.yaml b/config/crd/bases/cluster.kubernetes-fleet.io_memberclusters.yaml index a3120e983..9cdffdbed 100644 --- a/config/crd/bases/cluster.kubernetes-fleet.io_memberclusters.yaml +++ b/config/crd/bases/cluster.kubernetes-fleet.io_memberclusters.yaml @@ -408,6 +408,9 @@ spec: required: - spec type: object + x-kubernetes-validations: + - message: metadata.name max length is 63 + rule: size(self.metadata.name) < 64 served: true storage: false subresources: @@ -801,6 +804,9 @@ spec: required: - spec type: object + x-kubernetes-validations: + - message: metadata.name max length is 63 + rule: size(self.metadata.name) < 64 served: true storage: true subresources: diff --git a/test/e2e/resource_validation_test.go b/test/e2e/resource_validation_test.go new file mode 100644 index 000000000..b4f5ba82b --- /dev/null +++ b/test/e2e/resource_validation_test.go @@ -0,0 +1,219 @@ +/* +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. +*/ +package e2e + +import ( + "errors" + "fmt" + "reflect" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + rbacv1 "k8s.io/api/rbac/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + clusterv1beta1 "go.goms.io/fleet/apis/cluster/v1beta1" +) + +var _ = Describe("Resource validation tests for Member Cluster", func() { + It("should deny creating API with invalid name size", func() { + var name = "abcdef-123456789-123456789-123456789-123456789-123456789-123456789-123456789" + // Create the API. + memberClusterName := &clusterv1beta1.MemberCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: clusterv1beta1.MemberClusterSpec{ + Identity: rbacv1.Subject{ + Name: "fleet-member-agent-cluster-1", + Kind: "ServiceAccount", + Namespace: "fleet-system", + APIGroup: "", + }, + }, + } + By(fmt.Sprintf("expecting denial of CREATE API %s", name)) + err := hubClient.Create(ctx, memberClusterName) + var statusErr *k8serrors.StatusError + Expect(errors.As(err, &statusErr)).To(BeTrue(), fmt.Sprintf("Create API call produced error %s. Error type wanted is %s.", reflect.TypeOf(err), reflect.TypeOf(&k8serrors.StatusError{}))) + Expect(statusErr.Status().Message).Should(ContainSubstring("metadata.name max length is 63")) + }) + + It("should allow creating API with valid name size", func() { + var name = "abc-123456789-123456789-123456789-123456789-123456789-123456789" + // Create the API. + memberClusterName := &clusterv1beta1.MemberCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: clusterv1beta1.MemberClusterSpec{ + Identity: rbacv1.Subject{ + Name: "fleet-member-agent-cluster-1", + Kind: "ServiceAccount", + Namespace: "fleet-system", + APIGroup: "", + }, + }, + } + Expect(hubClient.Create(ctx, memberClusterName)).Should(Succeed()) + Expect(hubClient.Get(ctx, types.NamespacedName{Name: memberClusterName.Name}, memberClusterName)).Should(Succeed()) + ensureMemberClusterAndRelatedResourcesDeletion(name) + }) + + It("should deny creating API with invalid name starting with non-alphanumeric character", func() { + var name = "-abcdef-123456789-123456789-123456789-123456789-123456789" + // Create the API. + memberClusterName := &clusterv1beta1.MemberCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: clusterv1beta1.MemberClusterSpec{ + Identity: rbacv1.Subject{ + Name: "fleet-member-agent-cluster-1", + Kind: "ServiceAccount", + Namespace: "fleet-system", + APIGroup: "", + }, + }, + } + By(fmt.Sprintf("expecting denial of CREATE API %s", name)) + err := hubClient.Create(ctx, memberClusterName) + var statusErr *k8serrors.StatusError + Expect(errors.As(err, &statusErr)).To(BeTrue(), fmt.Sprintf("Create API call produced error %s. Error type wanted is %s.", reflect.TypeOf(err), reflect.TypeOf(&k8serrors.StatusError{}))) + Expect(statusErr.Status().Message).Should(ContainSubstring("a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")) + }) + + It("should allow creating API with valid name starting with alphabet character", func() { + var name = "abc-123456789" + // Create the API. + memberClusterName := &clusterv1beta1.MemberCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: clusterv1beta1.MemberClusterSpec{ + Identity: rbacv1.Subject{ + Name: "fleet-member-agent-cluster-1", + Kind: "ServiceAccount", + Namespace: "fleet-system", + APIGroup: "", + }, + }, + } + Expect(hubClient.Create(ctx, memberClusterName)).Should(Succeed()) + Expect(hubClient.Get(ctx, types.NamespacedName{Name: memberClusterName.Name}, memberClusterName)).Should(Succeed()) + ensureMemberClusterAndRelatedResourcesDeletion(name) + }) + + It("should allow creating API with valid name starting with numeric character", func() { + var name = "123-123456789" + // Create the API. + memberClusterName := &clusterv1beta1.MemberCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: clusterv1beta1.MemberClusterSpec{ + Identity: rbacv1.Subject{ + Name: "fleet-member-agent-cluster-1", + Kind: "ServiceAccount", + Namespace: "fleet-system", + APIGroup: "", + }, + }, + } + Expect(hubClient.Create(ctx, memberClusterName)).Should(Succeed()) + Expect(hubClient.Get(ctx, types.NamespacedName{Name: memberClusterName.Name}, memberClusterName)).Should(Succeed()) + ensureMemberClusterAndRelatedResourcesDeletion(name) + }) + + It("should deny creating API with invalid name ending with non-alphanumeric character", func() { + var name = "abcdef-123456789-123456789-123456789-123456789-123456789-" + // Create the API. + memberClusterName := &clusterv1beta1.MemberCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: clusterv1beta1.MemberClusterSpec{ + Identity: rbacv1.Subject{ + Name: "fleet-member-agent-cluster-1", + Kind: "ServiceAccount", + Namespace: "fleet-system", + APIGroup: "", + }, + }, + } + By(fmt.Sprintf("expecting denial of CREATE API %s", name)) + err := hubClient.Create(ctx, memberClusterName) + var statusErr *k8serrors.StatusError + Expect(errors.As(err, &statusErr)).To(BeTrue(), fmt.Sprintf("Create API call produced error %s. Error type wanted is %s.", reflect.TypeOf(err), reflect.TypeOf(&k8serrors.StatusError{}))) + Expect(statusErr.Status().Message).Should(ContainSubstring("a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")) + }) + + It("should allow creating API with valid name ending with alphabet character", func() { + var name = "123456789-abc" + // Create the API. + memberClusterName := &clusterv1beta1.MemberCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: clusterv1beta1.MemberClusterSpec{ + Identity: rbacv1.Subject{ + Name: "fleet-member-agent-cluster-1", + Kind: "ServiceAccount", + Namespace: "fleet-system", + APIGroup: "", + }, + }, + } + Expect(hubClient.Create(ctx, memberClusterName)).Should(Succeed()) + Expect(hubClient.Get(ctx, types.NamespacedName{Name: memberClusterName.Name}, memberClusterName)).Should(Succeed()) + ensureMemberClusterAndRelatedResourcesDeletion(name) + }) + + It("should allow creating API with valid name ending with numeric character", func() { + var name = "123456789-123" + // Create the API. + memberClusterName := &clusterv1beta1.MemberCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: clusterv1beta1.MemberClusterSpec{ + Identity: rbacv1.Subject{ + Name: "fleet-member-agent-cluster-1", + Kind: "ServiceAccount", + Namespace: "fleet-system", + APIGroup: "", + }, + }, + } + Expect(hubClient.Create(ctx, memberClusterName)).Should(Succeed()) + Expect(hubClient.Get(ctx, types.NamespacedName{Name: memberClusterName.Name}, memberClusterName)).Should(Succeed()) + ensureMemberClusterAndRelatedResourcesDeletion(name) + }) + + It("should deny creating API with invalid name containing character that is not alphanumeric and not -", func() { + var name = "a_bcdef-123456789-123456789-123456789-123456789-123456789-123456789-123456789" + // Create the API. + memberClusterName := &clusterv1beta1.MemberCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: clusterv1beta1.MemberClusterSpec{ + Identity: rbacv1.Subject{ + Name: "fleet-member-agent-cluster-1", + Kind: "ServiceAccount", + Namespace: "fleet-system", + APIGroup: "", + }, + }, + } + By(fmt.Sprintf("expecting denial of CREATE API %s", name)) + err := hubClient.Create(ctx, memberClusterName) + var statusErr *k8serrors.StatusError + Expect(errors.As(err, &statusErr)).To(BeTrue(), fmt.Sprintf("Create API call produced error %s. Error type wanted is %s.", reflect.TypeOf(err), reflect.TypeOf(&k8serrors.StatusError{}))) + Expect(statusErr.Status().Message).Should(ContainSubstring("a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")) + }) +})