Repository: clusternet/clusternet
Pull request #436 (2022-07-29 07:26:40)

add msc related crds
Signed-off-by: lmxia <xialingming@gmail.com>

<!--  Thanks for sending a pull request!

-->

#### What type of PR is this?
Feature add.
#### What this PR does / why we need it:
Deploy mcs related crds.
#### Which issue(s) this PR fixes:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->
Fixes #

#### Special notes for your reviewer:
Agent will have crds file. 

codecov-commenter: # [Codecov](https://codecov.io/gh/clusternet/clusternet/pull/436?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clusternet) Report
> Merging [#436](https://codecov.io/gh/clusternet/clusternet/pull/436?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clusternet) (18681e8) into [main](https://codecov.io/gh/clusternet/clusternet/commit/73325a49db0175bbc557e435a40c0cd71f33dab7?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clusternet) (73325a4) will **not change** coverage.
> The diff coverage is `n/a`.

```diff
@@           Coverage Diff           @@
##             main     #436   +/-   ##
=======================================
  Coverage   13.72%   13.72%           
=======================================
  Files          61       61           
  Lines        6595     6595           
=======================================
  Hits          905      905           
  Misses       5623     5623           
  Partials       67       67           
```



------

[Continue to review full report at Codecov](https://codecov.io/gh/clusternet/clusternet/pull/436?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clusternet).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clusternet)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/clusternet/clusternet/pull/436?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clusternet). Last update [73325a4...18681e8](https://codecov.io/gh/clusternet/clusternet/pull/436?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clusternet). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=clusternet).


Patch:
diff --git a/deploy/agent/multicluster.x-k8s.io_serviceexports.yaml b/deploy/agent/multicluster.x-k8s.io_serviceexports.yaml
new file mode 120000
index 0000000..9e1c3e2
--- /dev/null
+++ b/deploy/agent/multicluster.x-k8s.io_serviceexports.yaml
@@ -0,0 +1,1 @@
+../../manifests/crds/multicluster.x-k8s.io_serviceexports.yaml
\ No newline at end of file
diff --git a/deploy/agent/multicluster.x-k8s.io_serviceimports.yaml b/deploy/agent/multicluster.x-k8s.io_serviceimports.yaml
new file mode 120000
index 0000000..14e43a2
--- /dev/null
+++ b/deploy/agent/multicluster.x-k8s.io_serviceimports.yaml
@@ -0,0 +1,1 @@
+../../manifests/crds/multicluster.x-k8s.io_serviceimports.yaml
\ No newline at end of file
diff --git a/deploy/hub/multicluster.x-k8s.io_serviceexports.yaml b/deploy/hub/multicluster.x-k8s.io_serviceexports.yaml
new file mode 120000
index 0000000..9e1c3e2
--- /dev/null
+++ b/deploy/hub/multicluster.x-k8s.io_serviceexports.yaml
@@ -0,0 +1,1 @@
+../../manifests/crds/multicluster.x-k8s.io_serviceexports.yaml
\ No newline at end of file
diff --git a/deploy/hub/multicluster.x-k8s.io_serviceimports.yaml b/deploy/hub/multicluster.x-k8s.io_serviceimports.yaml
new file mode 120000
index 0000000..14e43a2
--- /dev/null
+++ b/deploy/hub/multicluster.x-k8s.io_serviceimports.yaml
@@ -0,0 +1,1 @@
+../../manifests/crds/multicluster.x-k8s.io_serviceimports.yaml
\ No newline at end of file
diff --git a/manifests/crds/multicluster.x-k8s.io_serviceexports.yaml b/manifests/crds/multicluster.x-k8s.io_serviceexports.yaml
new file mode 100644
index 0000000..8dda38c
--- /dev/null
+++ b/manifests/crds/multicluster.x-k8s.io_serviceexports.yaml
@@ -0,0 +1,133 @@
+# Copyright 2020 The Kubernetes Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  name: serviceexports.multicluster.x-k8s.io
+spec:
+  group: multicluster.x-k8s.io
+  scope: Namespaced
+  names:
+    plural: serviceexports
+    singular: serviceexport
+    kind: ServiceExport
+    shortNames:
+    - svcex
+  versions:
+  - name: v1alpha1
+    served: true
+    storage: true
+    subresources:
+      status: {}
+    additionalPrinterColumns:
+    - name: Age
+      type: date
+      jsonPath: .metadata.creationTimestamp
+    "schema":
+      "openAPIV3Schema":
+        description: ServiceExport declares that the Service with the same name and
+          namespace as this export should be consumable from other clusters.
+        type: object
+        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
+          status:
+            description: status describes the current state of an exported service.
+              Service configuration comes from the Service that had the same name
+              and namespace as this ServiceExport. Populated by the multi-cluster
+              service implementation's controller.
+            type: object
+            properties:
+              conditions:
+                type: array
+                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,
+                    type FooStatus struct{     // Represents the observations of a
+                    foo's current state.     // Known .status.conditions.type are:
+                    \"Available\", \"Progressing\", and \"Degraded\"     // +patchMergeKey=type
+                    \    // +patchStrategy=merge     // +listType=map     // +listMapKey=type
+                    \    Conditions []metav1.Condition `json:\"conditions,omitempty\"
+                    patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`
+                    \n     // other fields }"
+                  type: object
+                  required:
+                  - lastTransitionTime
+                  - message
+                  - reason
+                  - status
+                  - type
+                  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.
+                      type: string
+                      format: date-time
+                    message:
+                      description: message is a human readable message indicating
+                        details about the transition. This may be an empty string.
+                      type: string
+                      maxLength: 32768
+                    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.
+                      type: integer
+                      format: int64
+                      minimum: 0
+                    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.
+                      type: string
+                      maxLength: 1024
+                      minLength: 1
+                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                    status:
+                      description: status of the condition, one of True, False, Unknown.
+                      type: string
+                      enum:
+                      - "True"
+                      - "False"
+                      - Unknown
+                    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)
+                      type: string
+                      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])$
+                x-kubernetes-list-map-keys:
+                - type
+                x-kubernetes-list-type: map
diff --git a/manifests/crds/multicluster.x-k8s.io_serviceimports.yaml b/manifests/crds/multicluster.x-k8s.io_serviceimports.yaml
new file mode 100644
index 0000000..d0e26e9
--- /dev/null
+++ b/manifests/crds/multicluster.x-k8s.io_serviceimports.yaml
@@ -0,0 +1,161 @@
+# Copyright 2020 The Kubernetes Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  name: serviceimports.multicluster.x-k8s.io
+spec:
+  group: multicluster.x-k8s.io
+  scope: Namespaced
+  names:
+    plural: serviceimports
+    singular: serviceimport
+    kind: ServiceImport
+    shortNames:
+    - svcim
+  versions:
+  - name: v1alpha1
+    served: true
+    storage: true
+    subresources:
+      status: {}
+    additionalPrinterColumns:
+    - name: Type
+      type: string
+      description: The type of this ServiceImport
+      jsonPath: .spec.type
+    - name: IP
+      type: string
+      description: The VIP for this ServiceImport
+      jsonPath: .spec.ips
+    - name: Age
+      type: date
+      jsonPath: .metadata.creationTimestamp
+    "schema":
+      "openAPIV3Schema":
+        description: ServiceImport describes a service imported from clusters in a
+          ClusterSet.
+        type: object
+        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: spec defines the behavior of a ServiceImport.
+            type: object
+            required:
+            - ports
+            - type
+            properties:
+              ips:
+                description: ip will be used as the VIP for this service when type
+                  is ClusterSetIP.
+                type: array
+                maxItems: 1
+                items:
+                  type: string
+              ports:
+                type: array
+                items:
+                  description: ServicePort represents the port on which the service
+                    is exposed
+                  type: object
+                  required:
+                  - port
+                  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.
+                      type: integer
+                      format: int32
+                    protocol:
+                      description: The IP protocol for this port. Supports "TCP",
+                        "UDP", and "SCTP". Default is TCP.
+                      type: string
+                x-kubernetes-list-type: atomic
+              sessionAffinity:
+                description: 'Supports "ClientIP" and "None". Used to maintain session
+                  affinity. Enable client IP based session affinity. Must be ClientIP
+                  or None. Defaults to None. Ignored when type is Headless More info:
+                  https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies'
+                type: string
+              sessionAffinityConfig:
+                description: sessionAffinityConfig contains session affinity configuration.
+                type: object
+                properties:
+                  clientIP:
+                    description: clientIP contains the configurations of Client IP
+                      based session affinity.
+                    type: object
+                    properties:
+                      timeoutSeconds:
+                        description: timeoutSeconds specifies the seconds of ClientIP
+                          type session sticky time. The value must be >0 && <=86400(for
+                          1 day) if ServiceAffinity == "ClientIP". Default value is
+                          10800(for 3 hours).
+                        type: integer
+                        format: int32
+              type:
+                description: type defines the type of this service. Must be ClusterSetIP
+                  or Headless.
+                type: string
+                enum:
+                - ClusterSetIP
+                - Headless
+          status:
+            description: status contains information about the exported services that
+              form the multi-cluster service referenced by this ServiceImport.
+            type: object
+            properties:
+              clusters:
+                description: clusters is the list of exporting clusters from which
+                  this service was derived.
+                type: array
+                items:
+                  description: ClusterStatus contains service configuration mapped
+                    to a specific source cluster
+                  type: object
+                  required:
+                  - cluster
+                  properties:
+                    cluster:
+                      description: cluster is the name of the exporting cluster. Must
+                        be a valid RFC-1123 DNS label.
+                      type: string
+                x-kubernetes-list-map-keys:
+                - cluster
+                x-kubernetes-list-type: map
diff --git a/pkg/scheduler/framework/plugins/defaultbinder/default_binder.go b/pkg/scheduler/framework/plugins/defaultbinder/default_binder.go
index 5bb5770..d209fc0 100644
--- a/pkg/scheduler/framework/plugins/defaultbinder/default_binder.go
+++ b/pkg/scheduler/framework/plugins/defaultbinder/default_binder.go
@@ -18,10 +18,14 @@ package defaultbinder
 
 import (
 	"context"
+	"encoding/json"
+	"fmt"
 	"sort"
 
+	jsonpatch "github.com/evanphx/json-patch"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/klog/v2"
 
 	appsapi "github.com/clusternet/clusternet/pkg/apis/apps/v1alpha1"
@@ -61,7 +65,20 @@ func (pl *DefaultBinder) Bind(ctx context.Context, state *framework.CycleState,
 	subCopy.Status.SpecHash = utils.HashSubscriptionSpec(&subCopy.Spec)
 	subCopy.Status.DesiredReleases = len(targetClusters.BindingClusters)
 
-	_, err := pl.handle.ClientSet().AppsV1alpha1().Subscriptions(sub.Namespace).UpdateStatus(ctx, subCopy, metav1.UpdateOptions{})
+	oldData, err := json.Marshal(sub)
+	if err != nil {
+		return framework.AsStatus(fmt.Errorf("failed to marshal original subscription: %v", err))
+	}
+	newData, err := json.Marshal(subCopy)
+	if err != nil {
+		return framework.AsStatus(fmt.Errorf("failed to marshal new subscription: %v", err))
+	}
+	patchBytes, err := jsonpatch.CreateMergePatch(oldData, newData)
+	if err != nil {
+		return framework.AsStatus(fmt.Errorf("failed to create a merge patch: %v", err))
+	}
+
+	_, err = pl.handle.ClientSet().AppsV1alpha1().Subscriptions(sub.Namespace).Patch(ctx, sub.Name, types.MergePatchType, patchBytes, metav1.PatchOptions{}, "status")
 	if err != nil {
 		return framework.AsStatus(err)
 	}


Test patch:
diff --git a/pkg/scheduler/framework/plugins/defaultbinder/default_binder_test.go b/pkg/scheduler/framework/plugins/defaultbinder/default_binder_test.go
index e17ace7..fb92f8f 100644
--- a/pkg/scheduler/framework/plugins/defaultbinder/default_binder_test.go
+++ b/pkg/scheduler/framework/plugins/defaultbinder/default_binder_test.go
@@ -60,21 +60,16 @@ func TestDefaultBinder(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			var bindedClusters []string
+			var boundClusters []string
 			client := fake.NewSimpleClientset(testSubscription)
-			client.PrependReactor("update", "subscriptions", func(action clienttesting.Action) (bool, runtime.Object, error) {
+			client.PrependReactor("patch", "subscriptions", func(action clienttesting.Action) (bool, runtime.Object, error) {
 				if action.GetSubresource() != "status" {
 					return false, nil, nil
 				}
 				if tt.injectErr != nil {
 					return true, nil, tt.injectErr
 				}
-
-				bindedSubscription := action.(clienttesting.UpdateAction).GetObject().(*appsapi.Subscription)
-				if bindedSubscription != nil {
-					bindedClusters = bindedSubscription.Status.BindingClusters
-				}
-				return true, bindedSubscription, nil
+				return false, nil, nil
 			})
 
 			fh, err := frameworkruntime.NewFramework(nil, nil, frameworkruntime.WithClientSet(client))
@@ -83,10 +78,15 @@ func TestDefaultBinder(t *testing.T) {
 			}
 			binder := &DefaultBinder{handle: fh}
 			status := binder.Bind(context.Background(), nil, testSubscription, testClusters)
+			gotSubscription, err := client.AppsV1alpha1().Subscriptions(testSubscription.Namespace).Get(context.Background(), testSubscription.Name, metav1.GetOptions{})
+			if err != nil {
+				t.Errorf("failed to get subscription: %v", err)
+			}
+			boundClusters = gotSubscription.Status.BindingClusters
 			if got := status.AsError(); (tt.injectErr != nil) != (got != nil) {
 				t.Errorf("got error %q, want %q", got, tt.injectErr)
 			}
-			if diff := cmp.Diff(tt.wantedBindings, bindedClusters); diff != "" {
+			if diff := cmp.Diff(tt.wantedBindings, boundClusters); diff != "" {
 				t.Errorf("got different binding (-want, +got): %s", diff)
 			}
 		})