From eff274f2d77d685ee198e43d29296a688eb380bc Mon Sep 17 00:00:00 2001 From: jwcesign Date: Tue, 21 Nov 2023 15:51:27 +0800 Subject: [PATCH] api: define API for MultiClusterService Signed-off-by: jwcesign --- api/openapi-spec/swagger.json | 22 +++++++- ...rking.karmada.io_multiclusterservices.yaml | 25 +++++++++- pkg/apis/networking/v1alpha1/service_types.go | 27 ++++++++++ .../v1alpha1/zz_generated.deepcopy.go | 10 ++++ .../mcs_endpointslice_controller.go | 50 +++++++++++++++++++ pkg/generated/openapi/zz_generated.openapi.go | 39 ++++++++++++++- pkg/webhook/multiclusterservice/validating.go | 15 ++++-- .../multiclusterservice/validating_test.go | 27 ++++------ 8 files changed, 192 insertions(+), 23 deletions(-) create mode 100644 pkg/controllers/mcsendpointslice/mcs_endpointslice_controller.go diff --git a/api/openapi-spec/swagger.json b/api/openapi-spec/swagger.json index b5a97a07076e..2bf6ee1b0ffb 100644 --- a/api/openapi-spec/swagger.json +++ b/api/openapi-spec/swagger.json @@ -21160,6 +21160,18 @@ "types" ], "properties": { + "clientLocations": { + "description": "ServiceConsumers specifies the clusters which will request the service. If leave it empty, the service will be exposed to all clusters.", + "type": "array", + "items": { + "type": "string", + "default": "" + } + }, + "discoveryStrategy": { + "description": "DiscoveryStrategy specifies how to sync the EndpointSlice from one cluster to another cluster. Default to be RemoteAndLocal, which means syncing the EndpointSlice from one cluster to another cluster from the beginning, the requests to this service will routed to all of them.", + "type": "string" + }, "ports": { "description": "Ports is the list of ports that are exposed by this MultiClusterService. No specified port will be filtered out during the service exposure and discovery process. All ports in the referencing service will be exposed by default.", "type": "array", @@ -21169,10 +21181,18 @@ } }, "range": { - "description": "Range specifies the ranges where the referencing service should be exposed. Only valid and optional in case of Types contains CrossCluster. If not set and Types contains CrossCluster, all clusters will be selected, that means the referencing service will be exposed across all registered clusters.", + "description": "Range specifies the ranges where the referencing service should be exposed. Only valid and optional in case of Types contains CrossCluster. If not set and Types contains CrossCluster, all clusters will be selected, that means the referencing service will be exposed across all registered clusters. Deprecated: in favor of ServiceProviders/ServiceConsumers.", "default": {}, "$ref": "#/definitions/com.github.karmada-io.karmada.pkg.apis.networking.v1alpha1.ExposureRange" }, + "serverLocations": { + "description": "ServiceProviders specifies the clusters which will provide the service backend. If leave it empty, we will collect the backend endpoints from all clusters and sync them to the ClientLocations.", + "type": "array", + "items": { + "type": "string", + "default": "" + } + }, "types": { "description": "Types specifies how to expose the service referencing by this MultiClusterService.", "type": "array", diff --git a/charts/karmada/_crds/bases/networking/networking.karmada.io_multiclusterservices.yaml b/charts/karmada/_crds/bases/networking/networking.karmada.io_multiclusterservices.yaml index f23be4b59594..461b9d697290 100644 --- a/charts/karmada/_crds/bases/networking/networking.karmada.io_multiclusterservices.yaml +++ b/charts/karmada/_crds/bases/networking/networking.karmada.io_multiclusterservices.yaml @@ -44,6 +44,20 @@ spec: spec: description: Spec is the desired state of the MultiClusterService. properties: + clientLocations: + description: ServiceConsumers specifies the clusters which will request + the service. If leave it empty, the service will be exposed to all + clusters. + items: + type: string + type: array + discoveryStrategy: + description: DiscoveryStrategy specifies how to sync the EndpointSlice + from one cluster to another cluster. Default to be RemoteAndLocal, + which means syncing the EndpointSlice from one cluster to another + cluster from the beginning, the requests to this service will routed + to all of them. + type: string ports: description: Ports is the list of ports that are exposed by this MultiClusterService. No specified port will be filtered out during the service exposure @@ -66,11 +80,11 @@ spec: type: object type: array range: - description: Range specifies the ranges where the referencing service + description: 'Range specifies the ranges where the referencing service should be exposed. Only valid and optional in case of Types contains CrossCluster. If not set and Types contains CrossCluster, all clusters will be selected, that means the referencing service will be exposed - across all registered clusters. + across all registered clusters. Deprecated: in favor of ServiceProviders/ServiceConsumers.' properties: clusterNames: description: ClusterNames is the list of clusters to be selected. @@ -78,6 +92,13 @@ spec: type: string type: array type: object + serverLocations: + description: ServiceProviders specifies the clusters which will provide + the service backend. If leave it empty, we will collect the backend + endpoints from all clusters and sync them to the ClientLocations. + items: + type: string + type: array types: description: Types specifies how to expose the service referencing by this MultiClusterService. diff --git a/pkg/apis/networking/v1alpha1/service_types.go b/pkg/apis/networking/v1alpha1/service_types.go index 08f174469f27..0a1da0088127 100644 --- a/pkg/apis/networking/v1alpha1/service_types.go +++ b/pkg/apis/networking/v1alpha1/service_types.go @@ -53,16 +53,43 @@ type MultiClusterServiceSpec struct { // +optional Ports []ExposurePort `json:"ports,omitempty"` + // DiscoveryStrategy specifies how to sync the EndpointSlice from one cluster to another cluster. + // Default to be RemoteAndLocal, which means syncing the EndpointSlice from one cluster + // to another cluster from the beginning, the requests to this service will routed to all of them. + // +optional + DiscoveryStrategy ServiceDiscoveryStrategy `json:"discoveryStrategy,omitempty"` + // Range specifies the ranges where the referencing service should // be exposed. // Only valid and optional in case of Types contains CrossCluster. // If not set and Types contains CrossCluster, all clusters will // be selected, that means the referencing service will be exposed // across all registered clusters. + // Deprecated: in favor of ServiceProviders/ServiceConsumers. // +optional Range ExposureRange `json:"range,omitempty"` + + // ServiceProviders specifies the clusters which will provide the service backend. + // If leave it empty, we will collect the backend endpoints from all clusters and sync + // them to the ClientLocations. + // +optional + ServiceProviders []string `json:"serverLocations,omitempty"` + + // ServiceConsumers specifies the clusters which will request the service. + // If leave it empty, the service will be exposed to all clusters. + // +optional + ServiceConsumers []string `json:"clientLocations,omitempty"` } +// ServiceDiscoverStrategy describes how to sync the EndpointSlice between clusters. +type ServiceDiscoveryStrategy string + +const ( + // RemoteAndLocal means syncing the EndpointSlice of the service from one cluster + // to another cluster from the beginning, the requests to this service will routed to all of them. + RemoteAndLocal ServiceDiscoveryStrategy = "RemoteAndLocal" +) + // ExposureType describes how to expose the service. type ExposureType string diff --git a/pkg/apis/networking/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/networking/v1alpha1/zz_generated.deepcopy.go index 944d18eae51d..b447e90d01d1 100644 --- a/pkg/apis/networking/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/networking/v1alpha1/zz_generated.deepcopy.go @@ -182,6 +182,16 @@ func (in *MultiClusterServiceSpec) DeepCopyInto(out *MultiClusterServiceSpec) { copy(*out, *in) } in.Range.DeepCopyInto(&out.Range) + if in.ServiceProviders != nil { + in, out := &in.ServiceProviders, &out.ServiceProviders + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.ServiceConsumers != nil { + in, out := &in.ServiceConsumers, &out.ServiceConsumers + *out = make([]string, len(*in)) + copy(*out, *in) + } return } diff --git a/pkg/controllers/mcsendpointslice/mcs_endpointslice_controller.go b/pkg/controllers/mcsendpointslice/mcs_endpointslice_controller.go new file mode 100644 index 000000000000..4561673e7468 --- /dev/null +++ b/pkg/controllers/mcsendpointslice/mcs_endpointslice_controller.go @@ -0,0 +1,50 @@ +package mcsendpointslice + +import ( + "context" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/klog/v2" + controllerruntime "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + networkingv1alpha1 "github.com/karmada-io/karmada/pkg/apis/networking/v1alpha1" + "github.com/karmada-io/karmada/pkg/sharedcli/ratelimiterflag" +) + +const ControllerName = "mcs-endpointslice-controller" + +type MCSEndpointSliceController struct { + client.Client // used to operate ClusterResourceBinding resources. + + RateLimiterOptions ratelimiterflag.Options +} + +func (c *MCSEndpointSliceController) Reconcile(ctx context.Context, req controllerruntime.Request) (controllerruntime.Result, error) { + klog.V(4).Infof("Reconciling MultiClusterService %s.", req.NamespacedName.String()) + + mcs := &networkingv1alpha1.MultiClusterService{} + if err := c.Client.Get(ctx, req.NamespacedName, mcs); err != nil { + if apierrors.IsNotFound(err) { + return controllerruntime.Result{}, nil + } + + return controllerruntime.Result{}, err + } + + if !mcs.DeletionTimestamp.IsZero() { + return controllerruntime.Result{}, nil + } + + return controllerruntime.Result{}, nil +} + +// SetupWithManager creates a controller and register to controller manager. +func (c *MCSEndpointSliceController) SetupWithManager(mgr controllerruntime.Manager) error { + return controllerruntime.NewControllerManagedBy(mgr).For(&networkingv1alpha1.MultiClusterService{}). + WithEventFilter(predicate.GenerationChangedPredicate{}). + WithOptions(controller.Options{RateLimiter: ratelimiterflag.DefaultControllerRateLimiter(c.RateLimiterOptions)}). + Complete(c) +} diff --git a/pkg/generated/openapi/zz_generated.openapi.go b/pkg/generated/openapi/zz_generated.openapi.go index 2759b9b6bfec..085c7fddd3df 100755 --- a/pkg/generated/openapi/zz_generated.openapi.go +++ b/pkg/generated/openapi/zz_generated.openapi.go @@ -2964,13 +2964,50 @@ func schema_pkg_apis_networking_v1alpha1_MultiClusterServiceSpec(ref common.Refe }, }, }, + "discoveryStrategy": { + SchemaProps: spec.SchemaProps{ + Description: "DiscoveryStrategy specifies how to sync the EndpointSlice from one cluster to another cluster. Default to be RemoteAndLocal, which means syncing the EndpointSlice from one cluster to another cluster from the beginning, the requests to this service will routed to all of them.", + Type: []string{"string"}, + Format: "", + }, + }, "range": { SchemaProps: spec.SchemaProps{ - Description: "Range specifies the ranges where the referencing service should be exposed. Only valid and optional in case of Types contains CrossCluster. If not set and Types contains CrossCluster, all clusters will be selected, that means the referencing service will be exposed across all registered clusters.", + Description: "Range specifies the ranges where the referencing service should be exposed. Only valid and optional in case of Types contains CrossCluster. If not set and Types contains CrossCluster, all clusters will be selected, that means the referencing service will be exposed across all registered clusters. Deprecated: in favor of ServiceProviders/ServiceConsumers.", Default: map[string]interface{}{}, Ref: ref("github.com/karmada-io/karmada/pkg/apis/networking/v1alpha1.ExposureRange"), }, }, + "serverLocations": { + SchemaProps: spec.SchemaProps{ + Description: "ServiceProviders specifies the clusters which will provide the service backend. If leave it empty, we will collect the backend endpoints from all clusters and sync them to the ClientLocations.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "clientLocations": { + SchemaProps: spec.SchemaProps{ + Description: "ServiceConsumers specifies the clusters which will request the service. If leave it empty, the service will be exposed to all clusters.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, }, Required: []string{"types"}, }, diff --git a/pkg/webhook/multiclusterservice/validating.go b/pkg/webhook/multiclusterservice/validating.go index f77457154a7b..94c5a06bbd98 100644 --- a/pkg/webhook/multiclusterservice/validating.go +++ b/pkg/webhook/multiclusterservice/validating.go @@ -87,10 +87,19 @@ func (v *ValidatingAdmission) validateMultiClusterServiceSpec(mcs *networkingv1a exposureType := mcs.Spec.Types[i] allErrs = append(allErrs, v.validateExposureType(&exposureType, typePath)...) } - clusterNamesPath := specPath.Child("range").Child("clusterNames") - for i := range mcs.Spec.Range.ClusterNames { + clusterNamesPath := specPath.Child("range").Child("serverLocaltions") + for i := range mcs.Spec.ServiceProviders { clusterNamePath := clusterNamesPath.Index(i) - clusterName := mcs.Spec.Range.ClusterNames[i] + clusterName := mcs.Spec.ServiceProviders[i] + if errMegs := clustervalidation.ValidateClusterName(clusterName); len(errMegs) > 0 { + allErrs = append(allErrs, field.Invalid(clusterNamePath, clusterName, strings.Join(errMegs, ","))) + } + } + + clusterNamesPath = specPath.Child("range").Child("cllientLocaltions") + for i := range mcs.Spec.ServiceConsumers { + clusterNamePath := clusterNamesPath.Index(i) + clusterName := mcs.Spec.ServiceConsumers[i] if errMegs := clustervalidation.ValidateClusterName(clusterName); len(errMegs) > 0 { allErrs = append(allErrs, field.Invalid(clusterNamePath, clusterName, strings.Join(errMegs, ","))) } diff --git a/pkg/webhook/multiclusterservice/validating_test.go b/pkg/webhook/multiclusterservice/validating_test.go index 571521d86964..5fbc2fe7802d 100755 --- a/pkg/webhook/multiclusterservice/validating_test.go +++ b/pkg/webhook/multiclusterservice/validating_test.go @@ -37,9 +37,8 @@ func TestValidateMultiClusterServiceSpec(t *testing.T) { networkingv1alpha1.ExposureTypeLoadBalancer, networkingv1alpha1.ExposureTypeCrossCluster, }, - Range: networkingv1alpha1.ExposureRange{ - ClusterNames: []string{"member1", "member2"}, - }, + ServiceProviders: []string{"member1", "member2"}, + ServiceConsumers: []string{"member1", "member2"}, }, }, expectedErr: field.ErrorList{}, @@ -62,9 +61,8 @@ func TestValidateMultiClusterServiceSpec(t *testing.T) { networkingv1alpha1.ExposureTypeLoadBalancer, networkingv1alpha1.ExposureTypeLoadBalancer, }, - Range: networkingv1alpha1.ExposureRange{ - ClusterNames: []string{"member1"}, - }, + ServiceProviders: []string{"member1", "member2"}, + ServiceConsumers: []string{"member1", "member2"}, }, }, expectedErr: field.ErrorList{field.Duplicate(specFld.Child("ports").Index(1).Child("name"), "foo")}, @@ -82,9 +80,8 @@ func TestValidateMultiClusterServiceSpec(t *testing.T) { Types: []networkingv1alpha1.ExposureType{ networkingv1alpha1.ExposureTypeLoadBalancer, }, - Range: networkingv1alpha1.ExposureRange{ - ClusterNames: []string{"member1"}, - }, + ServiceProviders: []string{"member1", "member2"}, + ServiceConsumers: []string{"member1", "member2"}, }, }, expectedErr: field.ErrorList{field.Invalid(specFld.Child("ports").Index(0).Child("port"), int32(163121), validation.InclusiveRangeError(1, 65535))}, @@ -102,9 +99,8 @@ func TestValidateMultiClusterServiceSpec(t *testing.T) { Types: []networkingv1alpha1.ExposureType{ "", }, - Range: networkingv1alpha1.ExposureRange{ - ClusterNames: []string{"member1"}, - }, + ServiceProviders: []string{"member1", "member2"}, + ServiceConsumers: []string{"member1", "member2"}, }, }, expectedErr: field.ErrorList{field.Invalid(specFld.Child("types").Index(0), networkingv1alpha1.ExposureType(""), "ExposureType Error")}, @@ -122,12 +118,11 @@ func TestValidateMultiClusterServiceSpec(t *testing.T) { Types: []networkingv1alpha1.ExposureType{ networkingv1alpha1.ExposureTypeCrossCluster, }, - Range: networkingv1alpha1.ExposureRange{ - ClusterNames: []string{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, - }, + ServiceProviders: []string{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + ServiceConsumers: []string{}, }, }, - expectedErr: field.ErrorList{field.Invalid(specFld.Child("range").Child("clusterNames").Index(0), "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "must be no more than 48 characters")}, + expectedErr: field.ErrorList{field.Invalid(specFld.Child("range").Child("serverLocaltions").Index(0), "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "must be no more than 48 characters")}, }, } for _, tt := range tests {