diff --git a/api/openapi-spec/swagger.json b/api/openapi-spec/swagger.json index b5a97a07076e..2a70121d148f 100644 --- a/api/openapi-spec/swagger.json +++ b/api/openapi-spec/swagger.json @@ -20994,20 +20994,6 @@ } } }, - "com.github.karmada-io.karmada.pkg.apis.networking.v1alpha1.ExposureRange": { - "description": "ExposureRange describes a list of clusters where the service is exposed. Now supports selecting cluster by name, leave the room for extend more methods such as using label selector.", - "type": "object", - "properties": { - "clusterNames": { - "description": "ClusterNames is the list of clusters to be selected.", - "type": "array", - "items": { - "type": "string", - "default": "" - } - } - } - }, "com.github.karmada-io.karmada.pkg.apis.networking.v1alpha1.MultiClusterIngress": { "description": "MultiClusterIngress is a collection of rules that allow inbound connections to reach the endpoints defined by a backend. The structure of MultiClusterIngress is same as Ingress, indicates the Ingress in multi-clusters.", "type": "object", @@ -21160,6 +21146,18 @@ "types" ], "properties": { + "clientLocations": { + "description": "ClientLocations 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", @@ -21168,10 +21166,13 @@ "$ref": "#/definitions/com.github.karmada-io.karmada.pkg.apis.networking.v1alpha1.ExposurePort" } }, - "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.", - "default": {}, - "$ref": "#/definitions/com.github.karmada-io.karmada.pkg.apis.networking.v1alpha1.ExposureRange" + "serverLocations": { + "description": "ServerLocaltions specifies the clusters which will provide the service backend. If leave it empty, we will collect the backend pods' IP 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.", 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..a0fb3a728545 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: ClientLocations 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 @@ -65,19 +79,13 @@ spec: - port type: object type: array - 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. - properties: - clusterNames: - description: ClusterNames is the list of clusters to be selected. - items: - type: string - type: array - type: object + serverLocations: + description: ServerLocaltions specifies the clusters which will provide + the service backend. If leave it empty, we will collect the backend + pods' IP 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..99423be49846 100644 --- a/pkg/apis/networking/v1alpha1/service_types.go +++ b/pkg/apis/networking/v1alpha1/service_types.go @@ -53,16 +53,33 @@ type MultiClusterServiceSpec struct { // +optional Ports []ExposurePort `json:"ports,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. + // 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 - Range ExposureRange `json:"range,omitempty"` + DiscoveryStrategy ServiceDiscoveryStrategy `json:"discoveryStrategy,omitempty"` + + // ServerLocaltions specifies the clusters which will provide the service backend. + // If leave it empty, we will collect the backend pods' IP from all clusters and sync + // them to the ClientLocations. + // +optional + ServerLocaltions []string `json:"serverLocations,omitempty"` + + // ClientLocations specifies the clusters which will request the service. + // If leave it empty, the service will be exposed to all clusters. + // +optional + ClientLocations []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..47fb4d5491d9 100644 --- a/pkg/apis/networking/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/networking/v1alpha1/zz_generated.deepcopy.go @@ -181,7 +181,16 @@ func (in *MultiClusterServiceSpec) DeepCopyInto(out *MultiClusterServiceSpec) { *out = make([]ExposurePort, len(*in)) copy(*out, *in) } - in.Range.DeepCopyInto(&out.Range) + if in.ServerLocaltions != nil { + in, out := &in.ServerLocaltions, &out.ServerLocaltions + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.ClientLocations != nil { + in, out := &in.ClientLocations, &out.ClientLocations + *out = make([]string, len(*in)) + copy(*out, *in) + } return } diff --git a/pkg/generated/openapi/zz_generated.openapi.go b/pkg/generated/openapi/zz_generated.openapi.go index 2759b9b6bfec..ec99fa174ffc 100755 --- a/pkg/generated/openapi/zz_generated.openapi.go +++ b/pkg/generated/openapi/zz_generated.openapi.go @@ -2964,11 +2964,41 @@ func schema_pkg_apis_networking_v1alpha1_MultiClusterServiceSpec(ref common.Refe }, }, }, - "range": { + "discoveryStrategy": { 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.", - Default: map[string]interface{}{}, - Ref: ref("github.com/karmada-io/karmada/pkg/apis/networking/v1alpha1.ExposureRange"), + 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: "", + }, + }, + "serverLocations": { + SchemaProps: spec.SchemaProps{ + Description: "ServerLocaltions specifies the clusters which will provide the service backend. If leave it empty, we will collect the backend pods' IP 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: "ClientLocations 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: "", + }, + }, + }, }, }, }, @@ -2976,7 +3006,7 @@ func schema_pkg_apis_networking_v1alpha1_MultiClusterServiceSpec(ref common.Refe }, }, Dependencies: []string{ - "github.com/karmada-io/karmada/pkg/apis/networking/v1alpha1.ExposurePort", "github.com/karmada-io/karmada/pkg/apis/networking/v1alpha1.ExposureRange"}, + "github.com/karmada-io/karmada/pkg/apis/networking/v1alpha1.ExposurePort"}, } } diff --git a/pkg/webhook/multiclusterservice/validating.go b/pkg/webhook/multiclusterservice/validating.go index f77457154a7b..705437872248 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.ServerLocaltions { clusterNamePath := clusterNamesPath.Index(i) - clusterName := mcs.Spec.Range.ClusterNames[i] + clusterName := mcs.Spec.ServerLocaltions[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.ClientLocations { + clusterNamePath := clusterNamesPath.Index(i) + clusterName := mcs.Spec.ClientLocations[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..bf48c277aa8e 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"}, - }, + ServerLocaltions: []string{"member1", "member2"}, + ClientLocations: []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"}, - }, + ServerLocaltions: []string{"member1", "member2"}, + ClientLocations: []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"}, - }, + ServerLocaltions: []string{"member1", "member2"}, + ClientLocations: []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"}, - }, + ServerLocaltions: []string{"member1", "member2"}, + ClientLocations: []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"}, - }, + ServerLocaltions: []string{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + ClientLocations: []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 {