From f165ec24f348ad52023a29df97c8bc8cb334f7d4 Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Fri, 20 Oct 2023 14:44:21 -0700 Subject: [PATCH] allow fleet networking object --- go.mod | 1 + go.sum | 2 + .../fleetresourcehandler_webhook.go | 33 +-- pkg/webhook/validation/uservalidation.go | 12 +- test/e2e/v1alpha1/README.md | 2 +- test/e2e/v1alpha1/e2e_test.go | 2 + .../manifests/internalserviceexport-crd.yaml | 232 ++++++++++++++++++ test/e2e/v1alpha1/webhook_test.go | 77 +++++- 8 files changed, 333 insertions(+), 28 deletions(-) create mode 100644 test/e2e/v1alpha1/manifests/internalserviceexport-crd.yaml diff --git a/go.mod b/go.mod index b1d4813f9..9c6ddb6d4 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 + go.goms.io/fleet-networking v0.2.7 go.uber.org/atomic v1.11.0 go.uber.org/zap v1.24.0 golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 diff --git a/go.sum b/go.sum index 1872fd304..3919420f8 100644 --- a/go.sum +++ b/go.sum @@ -188,6 +188,8 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.goms.io/fleet-networking v0.2.7 h1:lVs2/GiCjo18BRgACib+VPnENUMh+2YbYXoeNtcAvw0= +go.goms.io/fleet-networking v0.2.7/go.mod h1:JoWG82La5nV29mooOnPpIhy6/Pi4oGXQk21CPF1UStg= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= diff --git a/pkg/webhook/fleetresourcehandler/fleetresourcehandler_webhook.go b/pkg/webhook/fleetresourcehandler/fleetresourcehandler_webhook.go index a5912fc74..d1f7aa732 100644 --- a/pkg/webhook/fleetresourcehandler/fleetresourcehandler_webhook.go +++ b/pkg/webhook/fleetresourcehandler/fleetresourcehandler_webhook.go @@ -7,6 +7,7 @@ import ( "regexp" "strings" + fleetnetworkingv1alpha1 "go.goms.io/fleet-networking/api/v1alpha1" admissionv1 "k8s.io/api/admission/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -29,7 +30,6 @@ const ( fleetMemberNamespacePrefix = "fleet-member" fleetNamespacePrefix = "fleet" kubeNamespacePrefix = "kube" - handleResourceFmt = "handling %s resource" ) // Add registers the webhook for K8s built-in object types. @@ -47,37 +47,36 @@ type fleetResourceValidator struct { // Handle receives the request then allows/denies the request to modify fleet resources. func (v *fleetResourceValidator) Handle(ctx context.Context, req admission.Request) admission.Response { - // special case for Kind:Namespace resources req.Name and req.Namespace has the same value the ObjectMeta.Name of Namespace. - if req.Kind.Kind == "Namespace" { - req.Namespace = "" - } namespacedName := types.NamespacedName{Name: req.Name, Namespace: req.Namespace} var response admission.Response if req.Operation == admissionv1.Create || req.Operation == admissionv1.Update || req.Operation == admissionv1.Delete { switch { case req.Kind == validation.CRDGVK: - klog.V(2).InfoS("handling CRD resource", "GVK", validation.CRDGVK, "namespacedName", namespacedName, "operation", req.Operation, "subResource", req.SubResource) + klog.V(2).InfoS("handling CRD resource", "name", req.Name, "operation", req.Operation, "subResource", req.SubResource) response = v.handleCRD(req) case req.Kind == validation.V1Alpha1MCGVK: - klog.V(2).InfoS("handling v1alpha1 member cluster resource", "GVK", validation.V1Alpha1MCGVK, "namespacedName", namespacedName, "operation", req.Operation, "subResource", req.SubResource) + klog.V(2).InfoS("handling v1alpha1 member cluster resource", "name", req.Name, "operation", req.Operation, "subResource", req.SubResource) response = v.handleV1Alpha1MemberCluster(req) case req.Kind == validation.MCGVK: - klog.V(2).InfoS("handling member cluster resource", "GVK", validation.MCGVK, "namespacedName", namespacedName, "operation", req.Operation, "subResource", req.SubResource) + klog.V(2).InfoS("handling member cluster resource", "name", req.Name, "operation", req.Operation, "subResource", req.SubResource) response = v.handleMemberCluster(req) case req.Kind == validation.NamespaceGVK: - klog.V(2).InfoS("handling namespace resource", "GVK", validation.NamespaceGVK, "namespacedName", namespacedName, "operation", req.Operation, "subResource", req.SubResource) + klog.V(2).InfoS("handling namespace resource", "name", req.Name, "operation", req.Operation, "subResource", req.SubResource) response = v.handleNamespace(req) case req.Kind == validation.V1Alpha1IMCGVK || req.Kind == validation.V1Alpha1WorkGVK || req.Kind == validation.IMCGVK || req.Kind == validation.WorkGVK: - klog.V(2).InfoS(fmt.Sprintf(handleResourceFmt, req.RequestKind.Kind), "GVK", req.RequestKind, "namespacedName", namespacedName, "operation", req.Operation, "subResource", req.SubResource) + klog.V(2).InfoS("handling fleet owned namespaced resource in system namespace", "GVK", req.RequestKind, "namespacedName", namespacedName, "operation", req.Operation, "subResource", req.SubResource) response = v.handleFleetMemberNamespacedResource(ctx, req) case req.Kind == validation.EventGVK: - klog.V(2).InfoS("handling event resource", "GVK", validation.EventGVK, "namespacedName", namespacedName, "operation", req.Operation, "subResource", req.SubResource) + klog.V(3).InfoS("handling event resource", "namespacedName", namespacedName, "operation", req.Operation, "subResource", req.SubResource) response = v.handleEvent(ctx, req) + case req.Kind.Group == fleetnetworkingv1alpha1.GroupVersion.Group && req.Kind.Version == fleetnetworkingv1alpha1.GroupVersion.Version: + klog.V(2).InfoS("handling fleet networking resource", "GVK", req.RequestKind, "namespacedName", namespacedName, "operation", req.Operation, "subResource", req.SubResource) + response = v.handleFleetNetworkingResources(req) case req.Namespace != "": - klog.V(2).InfoS(fmt.Sprintf(handleResourceFmt, req.RequestKind.Kind), "GVK", req.RequestKind, "namespacedName", namespacedName, "operation", req.Operation, "subResource", req.SubResource) + klog.V(2).InfoS("handling namespaced resource created in system namespace", "GVK", req.RequestKind, "namespacedName", namespacedName, "operation", req.Operation, "subResource", req.SubResource) response = validation.ValidateUserForResource(req, v.whiteListedUsers) default: - klog.V(2).InfoS("resource is not monitored by fleet resource validator webhook", "GVK", req.RequestKind, "namespacedName", namespacedName, "operation", req.Operation, "subResource", req.SubResource) + klog.V(3).InfoS("resource is not monitored by fleet resource validator webhook", "GVK", req.RequestKind, "namespacedName", namespacedName, "operation", req.Operation, "subResource", req.SubResource) response = admission.Allowed(fmt.Sprintf("user: %s in groups: %v is allowed to modify resource with GVK: %s", req.UserInfo.Username, req.UserInfo.Groups, req.Kind.String())) } } @@ -141,7 +140,7 @@ func (v *fleetResourceValidator) handleFleetMemberNamespacedResource(ctx context } return response } - klog.InfoS("namespace name doesn't begin with fleet-member prefix so we allow all operations on these namespaces", + klog.V(3).InfoS("namespace name doesn't begin with fleet-member prefix so we allow all operations on these namespaces", "user", req.UserInfo.Username, "groups", req.UserInfo.Groups, "operation", req.Operation, "kind", req.RequestKind.Kind, "subResource", req.SubResource, "namespacedName", types.NamespacedName{Name: req.Name, Namespace: req.Namespace}) return admission.Allowed("namespace name doesn't begin with fleet-member prefix so we allow all operations on these namespaces for the request object") } @@ -152,6 +151,12 @@ func (v *fleetResourceValidator) handleEvent(_ context.Context, _ admission.Requ return admission.Allowed("all events are allowed") } +// handleFleetNetworkingResources allows requests to modify fleet networking resources. +func (v *fleetResourceValidator) handleFleetNetworkingResources(_ admission.Request) admission.Response { + // TODO: add more check on the fleet networking resources. + return admission.Allowed("requests for fleet networking resources are all allowed") +} + // handlerNamespace allows/denies request to modify namespace after validation. func (v *fleetResourceValidator) handleNamespace(req admission.Request) admission.Response { fleetMatchResult := strings.HasPrefix(req.Name, fleetNamespacePrefix) diff --git a/pkg/webhook/validation/uservalidation.go b/pkg/webhook/validation/uservalidation.go index 55d3a1e29..2749457d0 100644 --- a/pkg/webhook/validation/uservalidation.go +++ b/pkg/webhook/validation/uservalidation.go @@ -60,7 +60,7 @@ func ValidateUserForFleetCRD(req admission.Request, whiteListedUsers []string, g klog.V(2).InfoS(deniedModifyResource, "user", userInfo.Username, "groups", userInfo.Groups, "operation", req.Operation, "GVK", req.RequestKind, "subResource", req.SubResource, "namespacedName", namespacedName) return admission.Denied(fmt.Sprintf(resourceDeniedFormat, userInfo.Username, userInfo.Groups, req.Operation, req.RequestKind, req.SubResource, namespacedName)) } - klog.V(2).InfoS(allowedModifyResource, "user", userInfo.Username, "groups", userInfo.Groups, "operation", req.Operation, "GVK", req.RequestKind, "subResource", req.SubResource, "namespacedName", namespacedName) + klog.V(3).InfoS(allowedModifyResource, "user", userInfo.Username, "groups", userInfo.Groups, "operation", req.Operation, "GVK", req.RequestKind, "subResource", req.SubResource, "namespacedName", namespacedName) return admission.Allowed(fmt.Sprintf(resourceAllowedFormat, userInfo.Username, userInfo.Groups, req.Operation, req.RequestKind, req.SubResource, namespacedName)) } @@ -69,7 +69,7 @@ func ValidateUserForResource(req admission.Request, whiteListedUsers []string) a namespacedName := types.NamespacedName{Name: req.Name, Namespace: req.Namespace} userInfo := req.UserInfo if isMasterGroupUserOrWhiteListedUser(whiteListedUsers, userInfo) || isUserAuthenticatedServiceAccount(userInfo) || isUserKubeScheduler(userInfo) || isUserKubeControllerManager(userInfo) || isNodeGroupUser(userInfo) { - klog.V(2).InfoS(allowedModifyResource, "user", userInfo.Username, "groups", userInfo.Groups, "operation", req.Operation, "GVK", req.RequestKind, "subResource", req.SubResource, "namespacedName", namespacedName) + klog.V(3).InfoS(allowedModifyResource, "user", userInfo.Username, "groups", userInfo.Groups, "operation", req.Operation, "GVK", req.RequestKind, "subResource", req.SubResource, "namespacedName", namespacedName) return admission.Allowed(fmt.Sprintf(resourceAllowedFormat, userInfo.Username, userInfo.Groups, req.Operation, req.RequestKind, req.SubResource, namespacedName)) } klog.V(2).InfoS(deniedModifyResource, "user", userInfo.Username, "groups", userInfo.Groups, "operation", req.Operation, "GVK", req.RequestKind, "subResource", req.SubResource, "namespacedName", namespacedName) @@ -89,7 +89,7 @@ func ValidateMemberClusterUpdate(currentObj, oldObj client.Object, req admission } if (isLabelUpdated || isAnnotationUpdated) && !isObjUpdated { // we allow any user to modify MemberCluster/Namespace labels/annotations. - klog.V(2).InfoS("user in groups is allowed to modify member cluster labels/annotations", "user", userInfo.Username, "groups", userInfo.Groups, "operation", req.Operation, "GVK", req.RequestKind, "subResource", req.SubResource, "namespacedName", namespacedName) + klog.V(3).InfoS("user in groups is allowed to modify member cluster labels/annotations", "user", userInfo.Username, "groups", userInfo.Groups, "operation", req.Operation, "GVK", req.RequestKind, "subResource", req.SubResource, "namespacedName", namespacedName) response = admission.Allowed(fmt.Sprintf(resourceAllowedFormat, userInfo.Username, userInfo.Groups, req.Operation, req.RequestKind, req.SubResource, namespacedName)) } if isObjUpdated { @@ -184,7 +184,7 @@ func ValidateMCIdentity(ctx context.Context, client client.Client, req admission var mc fleetv1alpha1.MemberCluster if err := client.Get(ctx, types.NamespacedName{Name: mcName}, &mc); err != nil { // fail open, if the webhook cannot get member cluster resources we don't block the request. - klog.V(2).ErrorS(err, fmt.Sprintf("failed to get v1alpha1 member cluster resource for request to modify %+v/%s, allowing request to be handled by api server", req.RequestKind, req.SubResource), + klog.ErrorS(err, fmt.Sprintf("failed to get v1alpha1 member cluster resource for request to modify %+v/%s, allowing request to be handled by api server", req.RequestKind, req.SubResource), "user", userInfo.Username, "groups", userInfo.Groups, "namespacedName", namespacedName) return admission.Allowed(fmt.Sprintf(resourceAllowedGetMCFailed, userInfo.Username, userInfo.Groups, req.Operation, req.RequestKind, req.SubResource, namespacedName)) } @@ -193,7 +193,7 @@ func ValidateMCIdentity(ctx context.Context, client client.Client, req admission var mc clusterv1beta1.MemberCluster if err := client.Get(ctx, types.NamespacedName{Name: mcName}, &mc); err != nil { // fail open, if the webhook cannot get member cluster resources we don't block the request. - klog.V(2).ErrorS(err, fmt.Sprintf("failed to get member cluster resource for request to modify %+v/%s, allowing request to be handled by api server", req.RequestKind, req.SubResource), + klog.ErrorS(err, fmt.Sprintf("failed to get member cluster resource for request to modify %+v/%s, allowing request to be handled by api server", req.RequestKind, req.SubResource), "user", userInfo.Username, "groups", userInfo.Groups, "namespacedName", namespacedName) return admission.Allowed(fmt.Sprintf(resourceAllowedGetMCFailed, userInfo.Username, userInfo.Groups, req.Operation, req.RequestKind, req.SubResource, namespacedName)) } @@ -202,7 +202,7 @@ func ValidateMCIdentity(ctx context.Context, client client.Client, req admission // For the upstream E2E we use hub agent service account's token which allows member agent to modify Work status, hence we use serviceAccountFmt to make the check. if identity == userInfo.Username || fmt.Sprintf(serviceAccountFmt, identity) == userInfo.Username { - klog.V(2).InfoS(allowedModifyResource, "user", userInfo.Username, "groups", userInfo.Groups, "operation", req.Operation, "GVK", req.RequestKind, "subResource", req.SubResource, "namespacedName", namespacedName) + klog.V(3).InfoS(allowedModifyResource, "user", userInfo.Username, "groups", userInfo.Groups, "operation", req.Operation, "GVK", req.RequestKind, "subResource", req.SubResource, "namespacedName", namespacedName) return admission.Allowed(fmt.Sprintf(resourceAllowedFormat, userInfo.Username, userInfo.Groups, req.Operation, req.RequestKind, req.SubResource, namespacedName)) } klog.V(2).InfoS(deniedModifyResource, "user", userInfo.Username, "groups", userInfo.Groups, "operation", req.Operation, "GVK", req.RequestKind, "subResource", req.SubResource, "namespacedName", namespacedName) diff --git a/test/e2e/v1alpha1/README.md b/test/e2e/v1alpha1/README.md index 21dfd1a5d..e0fa65eeb 100644 --- a/test/e2e/v1alpha1/README.md +++ b/test/e2e/v1alpha1/README.md @@ -30,7 +30,7 @@ test suites, follow the steps below: 5. Run the tests: ```shell - make run-e2e + make run-e2e-v1alpha1 ``` ## Access the `Kind` clusters diff --git a/test/e2e/v1alpha1/e2e_test.go b/test/e2e/v1alpha1/e2e_test.go index 451392f7c..3456e2deb 100644 --- a/test/e2e/v1alpha1/e2e_test.go +++ b/test/e2e/v1alpha1/e2e_test.go @@ -26,6 +26,7 @@ import ( clientgoscheme "k8s.io/client-go/kubernetes/scheme" workv1alpha1 "sigs.k8s.io/work-api/pkg/apis/v1alpha1" + fleetnetworkingv1alpha1 "go.goms.io/fleet-networking/api/v1alpha1" "go.goms.io/fleet/apis/v1alpha1" "go.goms.io/fleet/pkg/utils" "go.goms.io/fleet/test/e2e/framework" @@ -161,6 +162,7 @@ func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(v1alpha1.AddToScheme(scheme)) utilruntime.Must(workv1alpha1.AddToScheme(scheme)) + utilruntime.Must(fleetnetworkingv1alpha1.AddToScheme(scheme)) utilruntime.Must(apiextensionsv1.AddToScheme(scheme)) } diff --git a/test/e2e/v1alpha1/manifests/internalserviceexport-crd.yaml b/test/e2e/v1alpha1/manifests/internalserviceexport-crd.yaml new file mode 100644 index 000000000..7075e7193 --- /dev/null +++ b/test/e2e/v1alpha1/manifests/internalserviceexport-crd.yaml @@ -0,0 +1,232 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.7.0 + creationTimestamp: null + name: internalserviceexports.networking.fleet.azure.com +spec: + group: networking.fleet.azure.com + names: + categories: + - fleet-networking + kind: InternalServiceExport + listKind: InternalServiceExportList + plural: internalserviceexports + shortNames: + - internalsvcexport + singular: internalserviceexport + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: InternalServiceExport is a data transport type that member clusters + in the fleet use to upload the spec of exported Service to the hub cluster. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: InternalServiceExportSpec specifies the spec of an exported + Service; at this stage only the ports of an exported Service are sync'd. + properties: + ports: + description: A list of ports exposed by the exported Service. + items: + description: ServicePort represents the port on which the service + is exposed. + properties: + appProtocol: + description: The application protocol for this port. This field + follows standard Kubernetes label syntax. Un-prefixed names + are reserved for IANA standard service names (as per RFC-6335 + and http://www.iana.org/assignments/service-names). Non-standard + protocols should use prefixed names such as mycompany.com/my-custom-protocol. + Field can be enabled with ServiceAppProtocol feature gate. + type: string + name: + description: The name of this port within the service. This + must be a DNS_LABEL. All ports within a ServiceSpec must have + unique names. When considering the endpoints for a Service, + this must match the 'name' field in the EndpointPort. Optional + if only one ServicePort is defined on this service. + type: string + port: + description: The port that will be exposed by this service. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + protocol: + default: TCP + description: The IP protocol for this port. Supports "TCP", + "UDP", and "SCTP". Default is TCP. + enum: + - TCP + - UDP + - SCTP + type: string + targetPort: + anyOf: + - type: integer + - type: string + description: The port to access on the pods targeted by the + service. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: array + x-kubernetes-list-type: atomic + serviceReference: + description: The reference to the source Service. + properties: + apiVersion: + description: The API version of the referred object. + type: string + clusterId: + description: The ID of the cluster where the object is exported. + type: string + exportedSince: + description: The timestamp from a local clock when the generation + of the object is exported. This field is marked as optional + for backwards compatibility reasons. + format: date-time + type: string + generation: + description: The generation of the referred object. + format: int64 + type: integer + kind: + description: The kind of the referred object. + type: string + name: + description: The name of the referred object. + type: string + namespace: + description: The namespace of the referred object. + type: string + namespacedName: + description: The namespaced name of the referred object. + type: string + resourceVersion: + description: The resource version of the referred object. + type: string + uid: + description: The UID of the referred object. + type: string + required: + - clusterId + - generation + - kind + - name + - namespace + - namespacedName + - resourceVersion + - uid + type: object + required: + - ports + - serviceReference + type: object + status: + description: InternalServiceExportStatus contains the current status of + an InternalServiceExport. + properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n \ttype FooStatus struct{ \t // Represents the observations + of a foo's current state. \t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\" \t // + +patchMergeKey=type \t // +patchStrategy=merge \t // +listType=map + \t // +listMapKey=type \t Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n \t // other fields + \t}" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/test/e2e/v1alpha1/webhook_test.go b/test/e2e/v1alpha1/webhook_test.go index 7f75fa56e..715bb6e66 100644 --- a/test/e2e/v1alpha1/webhook_test.go +++ b/test/e2e/v1alpha1/webhook_test.go @@ -11,6 +11,7 @@ import ( "fmt" "reflect" "regexp" + "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -18,7 +19,7 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" - v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" k8sErrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -29,6 +30,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" workv1alpha1 "sigs.k8s.io/work-api/pkg/apis/v1alpha1" + fleetnetworkingv1alpha1 "go.goms.io/fleet-networking/api/v1alpha1" + + fleetv1beta1 "go.goms.io/fleet/apis/placement/v1beta1" fleetv1alpha1 "go.goms.io/fleet/apis/v1alpha1" "go.goms.io/fleet/pkg/utils" testutils "go.goms.io/fleet/test/e2e/v1alpha1/utils" @@ -57,7 +61,7 @@ const ( ) var ( - crdGVK = metav1.GroupVersionKind{Group: v1.SchemeGroupVersion.Group, Version: v1.SchemeGroupVersion.Version, Kind: "CustomResourceDefinition"} + crdGVK = metav1.GroupVersionKind{Group: apiextensionsv1.SchemeGroupVersion.Group, Version: apiextensionsv1.SchemeGroupVersion.Version, Kind: "CustomResourceDefinition"} mcGVK = metav1.GroupVersionKind{Group: fleetv1alpha1.GroupVersion.Group, Version: fleetv1alpha1.GroupVersion.Version, Kind: "MemberCluster"} imcGVK = metav1.GroupVersionKind{Group: fleetv1alpha1.GroupVersion.Group, Version: fleetv1alpha1.GroupVersion.Version, Kind: "InternalMemberCluster"} namespaceGVK = metav1.GroupVersionKind{Group: corev1.SchemeGroupVersion.Group, Version: corev1.SchemeGroupVersion.Version, Kind: "Namespace"} @@ -510,7 +514,7 @@ var _ = Describe("Fleet's Hub cluster webhook tests", func() { var _ = Describe("Fleet's CRD Resource Handler webhook tests", func() { Context("CRD validation webhook", func() { It("should deny CREATE operation on Fleet CRD for user not in system:masters group", func() { - var crd v1.CustomResourceDefinition + var crd apiextensionsv1.CustomResourceDefinition Expect(utils.GetObjectFromManifest("./config/crd/bases/fleet.azure.com_clusterresourceplacements.yaml", &crd)).Should(Succeed()) By("expecting denial of operation CREATE of CRD") @@ -522,7 +526,7 @@ var _ = Describe("Fleet's CRD Resource Handler webhook tests", func() { It("should deny UPDATE operation on Fleet CRD for user not in system:masters group", func() { Eventually(func(g Gomega) error { - var crd v1.CustomResourceDefinition + var crd apiextensionsv1.CustomResourceDefinition g.Expect(HubCluster.KubeClient.Get(ctx, types.NamespacedName{Name: "memberclusters.fleet.azure.com"}, &crd)).Should(Succeed()) By("update labels in CRD") labels := crd.GetLabels() @@ -541,7 +545,7 @@ var _ = Describe("Fleet's CRD Resource Handler webhook tests", func() { }) It("should deny DELETE operation on Fleet CRD for user not in system:masters group", func() { - crd := v1.CustomResourceDefinition{ + crd := apiextensionsv1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: "works.multicluster.x-k8s.io", }, @@ -554,7 +558,7 @@ var _ = Describe("Fleet's CRD Resource Handler webhook tests", func() { }) It("should allow UPDATE operation on Fleet CRDs even if user in system:masters group", func() { - var crd v1.CustomResourceDefinition + var crd apiextensionsv1.CustomResourceDefinition Eventually(func(g Gomega) error { g.Expect(HubCluster.KubeClient.Get(ctx, types.NamespacedName{Name: "memberclusters.fleet.azure.com"}, &crd)).Should(Succeed()) @@ -579,7 +583,7 @@ var _ = Describe("Fleet's CRD Resource Handler webhook tests", func() { }) It("should allow CREATE operation on Other CRDs", func() { - var crd v1.CustomResourceDefinition + var crd apiextensionsv1.CustomResourceDefinition Expect(utils.GetObjectFromManifest("./test/integration/manifests/resources/test_clonesets_crd.yaml", &crd)).Should(Succeed()) By("expecting error to be nil") @@ -1443,3 +1447,62 @@ var _ = Describe("Fleet's Reserved Namespace Handler webhook tests", func() { }) }) }) + +var _ = Describe("Fleet's Reserved Namespace Handler fleet network tests", Ordered, func() { + Context("allow requests to modify fleet networking resources", Ordered, func() { + var internalServiceExportCRD apiextensionsv1.CustomResourceDefinition + var ns corev1.Namespace + + BeforeAll(func() { + By("Create internalServiceExport CRD") + Expect(utils.GetObjectFromManifest("./test/e2e/v1alpha1/manifests/internalserviceexport-crd.yaml", &internalServiceExportCRD)).Should(Succeed()) + Expect(HubCluster.KubeClient.Create(ctx, &internalServiceExportCRD)).Should(Succeed()) + By("internalServiceExport CRD created") + + ns = corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fleet-member-test-internal-service-export", + Labels: map[string]string{fleetv1beta1.FleetResourceLabelKey: "true"}, + }, + } + Expect(HubCluster.KubeClient.Create(ctx, &ns)).Should(Succeed()) + By(fmt.Sprintf("namespace `%s` is created", ns.Name)) + }) + + It("should allow CREATE operation on Internal service export resource in fleet-member namespace for user not in system:masters group", func() { + ise := fleetnetworkingv1alpha1.InternalServiceExport{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-internal-service-export", + Namespace: ns.Name, + }, + Spec: fleetnetworkingv1alpha1.InternalServiceExportSpec{ + Ports: []fleetnetworkingv1alpha1.ServicePort{ + { + Protocol: corev1.ProtocolTCP, + Port: 4848, + }, + }, + ServiceReference: fleetnetworkingv1alpha1.ExportedObjectReference{ + NamespacedName: "test-svc", + ResourceVersion: "test-resource-version", + ClusterID: "member-1", + ExportedSince: metav1.NewTime(time.Now().Round(time.Second)), + }, + }, + } + By("expecting successful CREATE of Internal Service Export") + Expect(HubCluster.ImpersonateKubeClient.Create(ctx, &ise)).Should(Succeed()) + By("expecting successful DELETE of namespace") + Expect(HubCluster.KubeClient.Delete(ctx, &ns)).Should(Succeed()) + }) + + AfterAll(func() { + Expect(HubCluster.KubeClient.Delete(ctx, &ns)).Should(Succeed()) + By(fmt.Sprintf("namespace %s is deleted", ns.Name)) + + Expect(HubCluster.KubeClient.Delete(ctx, &internalServiceExportCRD)).Should(Succeed()) + By(fmt.Sprintf("crd %s is deleted", internalServiceExportCRD.Name)) + + }) + }) +})