Repository: openkruise/kruise-game

Issue:
ClusterIP / Headless Service 类型网络希望在原地升级时不断流
在原地热更过程中， Pod 因为代码热更 sidecar 重启而导致变成了 not ready 状态，
我们有自建使用的 clusterIP service / headless service，在这个过程中，会导致断流（ pod 状态变为 not ready 会从对应的 endpoint 摘除 ）

目前 SLB 可以被 GameServerSet 托管，声明 AllowNotReadyContainers 达到不断流的目的，如下所示
```
    - name: AllowNotReadyContainers
       value: SidecarName1,SidecarName2
```
我们使用的 SLB 仅用来做南北流量入口，客户端访问通过 SLB 进入
但是服务之间的内部的东西流量调用还在 K8S 中，还是使用  clusterIP service / headless service，当在执行原地热更升级过程中，会导致服务之间调用出现问题，希望能支持下

Discussion:
@chrisliu1995: [enhance] 

在 AlibabaCloud-SLB-SharedPort 网络模型之上新增参数 `ManagedServiceNames` / `ManagedServiceSelector`：

ManagedServiceNames：填写与gss同命名空间下希望被托管的服务名称列表。这些service将被kruise game controller管理，支持在原地升级时不断流


ManagedServiceSelector：对应上述service的label selector，用于在网络隔离时针对性将对应endpoint摘除（以及恢复）。这里的selector不可以与gss默认的selector重复【即，game.kruise.io/owner-gss:{gssName}】

Solution patch:
diff --git a/cloudprovider/alibabacloud/slb_sp.go b/cloudprovider/alibabacloud/slb_sp.go
index da3d31a..9391d00 100644
--- a/cloudprovider/alibabacloud/slb_sp.go
+++ b/cloudprovider/alibabacloud/slb_sp.go
@@ -21,8 +21,10 @@ import (
 )
 
 const (
-	SlbSPNetwork  = "AlibabaCloud-SLB-SharedPort"
-	SvcSLBSPLabel = "game.kruise.io/AlibabaCloud-SLB-SharedPort"
+	SlbSPNetwork                     = "AlibabaCloud-SLB-SharedPort"
+	SvcSLBSPLabel                    = "game.kruise.io/AlibabaCloud-SLB-SharedPort"
+	ManagedServiceNamesConfigName    = "ManagedServiceNames"
+	ManagedServiceSelectorConfigName = "ManagedServiceSelector"
 )
 
 const (
@@ -43,9 +45,12 @@ type SlbSpPlugin struct {
 }
 
 type lbSpConfig struct {
-	lbIds     []string
-	ports     []int
-	protocols []corev1.Protocol
+	lbIds                       []string
+	ports                       []int
+	protocols                   []corev1.Protocol
+	managedServiceNames         []string
+	managedServiceSelectorKey   string
+	managedServiceSelectorValue string
 }
 
 func (s *SlbSpPlugin) OnPodAdded(c client.Client, pod *corev1.Pod, ctx context.Context) (*corev1.Pod, cperrors.PluginError) {
@@ -112,6 +117,7 @@ func (s *SlbSpPlugin) OnPodUpdated(c client.Client, pod *corev1.Pod, ctx context
 	if networkManager.GetNetworkDisabled() && hasLabel {
 		newLabels := pod.GetLabels()
 		delete(newLabels, SlbIdLabelKey)
+		delete(newLabels, podNetConfig.managedServiceSelectorKey)
 		pod.Labels = newLabels
 		networkStatus.CurrentNetworkState = gamekruiseiov1alpha1.NetworkNotReady
 		pod, err = networkManager.UpdateNetworkStatus(*networkStatus, pod)
@@ -121,6 +127,7 @@ func (s *SlbSpPlugin) OnPodUpdated(c client.Client, pod *corev1.Pod, ctx context
 	// enable network
 	if !networkManager.GetNetworkDisabled() && !hasLabel {
 		pod.Labels[SlbIdLabelKey] = podSlbId
+		pod.Labels[podNetConfig.managedServiceSelectorKey] = podNetConfig.managedServiceSelectorValue
 		networkStatus.CurrentNetworkState = gamekruiseiov1alpha1.NetworkReady
 		pod, err = networkManager.UpdateNetworkStatus(*networkStatus, pod)
 		return pod, cperrors.ToPluginError(err, cperrors.InternalError)
@@ -144,6 +151,26 @@ func (s *SlbSpPlugin) OnPodUpdated(c client.Client, pod *corev1.Pod, ctx context
 				return pod, cperrors.ToPluginError(err, cperrors.ApiCallError)
 			}
 		}
+		for _, svcName := range podNetConfig.managedServiceNames {
+			managedSvc := &corev1.Service{}
+			getErr := c.Get(ctx, types.NamespacedName{
+				Namespace: pod.GetNamespace(),
+				Name:      svcName,
+			}, managedSvc)
+			if getErr != nil {
+				return pod, cperrors.ToPluginError(err, cperrors.ApiCallError)
+			}
+			toUpDateManagedSvc, err := utils.AllowNotReadyContainers(c, ctx, pod, managedSvc, true)
+			if err != nil {
+				return pod, err
+			}
+			if toUpDateManagedSvc {
+				err := c.Update(ctx, managedSvc)
+				if err != nil {
+					return pod, cperrors.ToPluginError(err, cperrors.ApiCallError)
+				}
+			}
+		}
 	}
 
 	// network ready
@@ -311,18 +338,29 @@ func parseLbSpConfig(conf []gamekruiseiov1alpha1.NetworkConfParams) *lbSpConfig
 	var lbIds []string
 	var ports []int
 	var protocols []corev1.Protocol
+	var managedServiceNames []string
+	var managedServiceSelectorKey string
+	var managedServiceSelectorValue string
 	for _, c := range conf {
 		switch c.Name {
 		case SlbIdsConfigName:
 			lbIds = parseLbIds(c.Value)
 		case PortProtocolsConfigName:
 			ports, protocols = parsePortProtocols(c.Value)
+		case ManagedServiceNamesConfigName:
+			managedServiceNames = strings.Split(c.Value, ",")
+		case ManagedServiceSelectorConfigName:
+			managedServiceSelectorKey = strings.Split(c.Value, "=")[0]
+			managedServiceSelectorValue = strings.Split(c.Value, "=")[1]
 		}
 	}
 	return &lbSpConfig{
-		lbIds:     lbIds,
-		ports:     ports,
-		protocols: protocols,
+		lbIds:                       lbIds,
+		ports:                       ports,
+		protocols:                   protocols,
+		managedServiceNames:         managedServiceNames,
+		managedServiceSelectorKey:   managedServiceSelectorKey,
+		managedServiceSelectorValue: managedServiceSelectorValue,
 	}
 }
 
diff --git a/cloudprovider/kubernetes/hostPort.go b/cloudprovider/kubernetes/hostPort.go
index 3603118..6710eae 100644
--- a/cloudprovider/kubernetes/hostPort.go
+++ b/cloudprovider/kubernetes/hostPort.go
@@ -42,6 +42,7 @@ const (
 	//Its corresponding value format is as follows, containerName:port1/protocol1,port2/protocol2,... e.g. game-server:25565/TCP
 	//When no protocol is specified, TCP is used by default
 	ContainerPortsKey = "ContainerPorts"
+	PortSameAsHost    = "SameAsHost"
 )
 
 type HostPortPlugin struct {
@@ -103,6 +104,10 @@ func (hpp *HostPortPlugin) OnPodAdded(c client.Client, pod *corev1.Pod, ctx cont
 		if ports, ok := containerPortsMap[container.Name]; ok {
 			containerPorts := container.Ports
 			for i, port := range ports {
+				// -1 means same as host
+				if port == -1 {
+					port = hostPorts[numToAlloc-1]
+				}
 				containerPort := corev1.ContainerPort{
 					ContainerPort: port,
 					HostPort:      hostPorts[numToAlloc-1],
@@ -338,9 +343,15 @@ func parseConfig(conf []gamekruiseiov1alpha1.NetworkConfParams, pod *corev1.Pod)
 				for _, portString := range strings.Split(cpSlice[1], ",") {
 					ppSlice := strings.Split(portString, "/")
 					// handle port
-					port, err := strconv.ParseInt(ppSlice[0], 10, 32)
-					if err != nil {
-						continue
+					var port int64
+					var err error
+					if ppSlice[0] == PortSameAsHost {
+						port = -1
+					} else {
+						port, err = strconv.ParseInt(ppSlice[0], 10, 32)
+						if err != nil {
+							continue
+						}
 					}
 					numToAlloc++
 					ports = append(ports, int32(port))
diff --git a/pkg/controllers/gameserverset/gameserverset_manager.go b/pkg/controllers/gameserverset/gameserverset_manager.go
index 81a355b..56bd05a 100644
--- a/pkg/controllers/gameserverset/gameserverset_manager.go
+++ b/pkg/controllers/gameserverset/gameserverset_manager.go
@@ -372,6 +372,7 @@ func (manager *GameServerSetManager) SyncPodProbeMarker() (error, bool) {
 	// update ppm
 	if util.GetHash(gss.Spec.ServiceQualities) != ppm.GetAnnotations()[gameKruiseV1alpha1.PpmHashKey] {
 		ppm.Spec.Probes = constructProbes(gss)
+		ppm.Annotations[gameKruiseV1alpha1.PpmHashKey] = util.GetHash(gss.Spec.ServiceQualities)
 		by, _ := json.Marshal(ppm.Spec.Probes)
 		manager.eventRecorder.Event(gss, corev1.EventTypeNormal, UpdatePPMReason, "update PodProbeMarker")
 		klog.Infof("GameserverSet(%s/%s) update PodProbeMarker(%s) body(%s)", gss.Namespace, gss.Name, ppm.Name, string(by))
diff --git a/pkg/externalscaler/externalscaler.go b/pkg/externalscaler/externalscaler.go
index 5e77307..8283475 100644
--- a/pkg/externalscaler/externalscaler.go
+++ b/pkg/externalscaler/externalscaler.go
@@ -42,6 +42,21 @@ func (e *ExternalScaler) IsActive(ctx context.Context, scaledObjectRef *ScaledOb
 	if numWaitToBeDeleted == nil {
 		return nil, fmt.Errorf("GameServerSet %s/%s has not inited", ns, name)
 	}
+
+	minNum := 0.0
+	minNumStr := scaledObjectRef.GetScalerMetadata()[NoneGameServerMinNumberKey]
+	if minNumStr != "" {
+		minNum, err = strconv.ParseFloat(minNumStr, 32)
+		if err != nil {
+			return nil, err
+		}
+	}
+	if minNum > 0.0 {
+		return &IsActiveResponse{
+			Result: true,
+		}, nil
+	}
+
 	desireReplicas := currentReplicas - *numWaitToBeDeleted
 	return &IsActiveResponse{
 		Result: desireReplicas > 0,
@@ -197,7 +212,7 @@ func handleMinNum(totalNum, noneNum int, minNumStr string) (int, error) {
 		delta = math.Round(delta*100) / 100
 		minNum := int(math.Ceil(delta)) + noneNum
 		return minNum, nil
-	case n >= 1:
+	case n >= 1 || n == 0:
 		n = math.Ceil(n)
 		return int(n), nil
 	}



Test patch:
diff --git a/cloudprovider/alibabacloud/slb_sp_test.go b/cloudprovider/alibabacloud/slb_sp_test.go
index 8d2a81e..36351d1 100644
--- a/cloudprovider/alibabacloud/slb_sp_test.go
+++ b/cloudprovider/alibabacloud/slb_sp_test.go
@@ -106,11 +106,22 @@ func TestParseLbSpConfig(t *testing.T) {
 					Name:  SlbIdsConfigName,
 					Value: "lb-xxa",
 				},
+				{
+					Name:  ManagedServiceNamesConfigName,
+					Value: "service-clusterIp",
+				},
+				{
+					Name:  ManagedServiceSelectorConfigName,
+					Value: "game=v1",
+				},
 			},
 			podNetConfig: &lbSpConfig{
-				lbIds:     []string{"lb-xxa"},
-				ports:     []int{80},
-				protocols: []corev1.Protocol{corev1.ProtocolTCP},
+				lbIds:                       []string{"lb-xxa"},
+				ports:                       []int{80},
+				protocols:                   []corev1.Protocol{corev1.ProtocolTCP},
+				managedServiceNames:         []string{"service-clusterIp"},
+				managedServiceSelectorKey:   "game",
+				managedServiceSelectorValue: "v1",
 			},
 		},
 	}
diff --git a/pkg/externalscaler/externalscaler_test.go b/pkg/externalscaler/externalscaler_test.go
index 081fbaf..dfb94ac 100644
--- a/pkg/externalscaler/externalscaler_test.go
+++ b/pkg/externalscaler/externalscaler_test.go
@@ -102,7 +102,7 @@ func TestHandleMinNum(t *testing.T) {
 			noneNum:   2,
 			minNumStr: "0",
 			wantMin:   0,
-			wantErr:   true,
+			wantErr:   false,
 		},
 		{
 			name:      "invalid n - negative",