diff --git a/apis/v1alpha1/gameserver_types.go b/apis/v1alpha1/gameserver_types.go index 5310e86e..bc29fd06 100644 --- a/apis/v1alpha1/gameserver_types.go +++ b/apis/v1alpha1/gameserver_types.go @@ -102,8 +102,39 @@ type GameServerStatus struct { UpdatePriority *intstr.IntOrString `json:"updatePriority,omitempty"` DeletionPriority *intstr.IntOrString `json:"deletionPriority,omitempty"` LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"` + // Conditions is an array of current observed GameServer conditions. + // +optional + Conditions []GameServerCondition `json:"conditions,omitempty" ` } +type GameServerCondition struct { + // Type is the type of the condition. + Type GameServerConditionType `json:"type"` + // Status is the status of the condition. + // Can be True, False, Unknown. + Status corev1.ConditionStatus `json:"status"` + // Last time we probed the condition. + // +optional + LastProbeTime metav1.Time `json:"lastProbeTime,omitempty"` + // Last time the condition transitioned from one status to another. + // +optional + LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"` + // Unique, one-word, CamelCase reason for the condition's last transition. + // +optional + Reason string `json:"reason,omitempty"` + // Human-readable message indicating details about last transition. + // +optional + Message string `json:"message,omitempty"` +} + +type GameServerConditionType string + +const ( + NodeNormal GameServerConditionType = "NodeNormal" + PersistentVolumeNormal GameServerConditionType = "PersistentVolumeNormal" + PodNormal GameServerConditionType = "PodNormal" +) + type NetworkStatus struct { NetworkType string `json:"networkType,omitempty"` InternalAddresses []NetworkAddress `json:"internalAddresses,omitempty"` diff --git a/apis/v1alpha1/zz_generated.deepcopy.go b/apis/v1alpha1/zz_generated.deepcopy.go index 5cabd0ae..5f93d076 100644 --- a/apis/v1alpha1/zz_generated.deepcopy.go +++ b/apis/v1alpha1/zz_generated.deepcopy.go @@ -55,6 +55,23 @@ func (in *GameServer) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GameServerCondition) DeepCopyInto(out *GameServerCondition) { + *out = *in + in.LastProbeTime.DeepCopyInto(&out.LastProbeTime) + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GameServerCondition. +func (in *GameServerCondition) DeepCopy() *GameServerCondition { + if in == nil { + return nil + } + out := new(GameServerCondition) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GameServerList) DeepCopyInto(out *GameServerList) { *out = *in @@ -259,6 +276,13 @@ func (in *GameServerStatus) DeepCopyInto(out *GameServerStatus) { **out = **in } in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]GameServerCondition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GameServerStatus. diff --git a/config/crd/bases/game.kruise.io_gameservers.yaml b/config/crd/bases/game.kruise.io_gameservers.yaml index 69fbfbe8..590167ee 100644 --- a/config/crd/bases/game.kruise.io_gameservers.yaml +++ b/config/crd/bases/game.kruise.io_gameservers.yaml @@ -76,6 +76,40 @@ spec: status: description: GameServerStatus defines the observed state of GameServer properties: + conditions: + description: Conditions is an array of current observed GameServer + conditions. + items: + properties: + lastProbeTime: + description: Last time we probed the condition. + format: date-time + type: string + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. + format: date-time + type: string + message: + description: Human-readable message indicating details about + last transition. + type: string + reason: + description: Unique, one-word, CamelCase reason for the condition's + last transition. + type: string + status: + description: Status is the status of the condition. Can be True, + False, Unknown. + type: string + type: + description: Type is the type of the condition. + type: string + required: + - status + - type + type: object + type: array currentState: type: string deletionPriority: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index cb56cd6e..1be86288 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -118,6 +118,34 @@ rules: - nodes/status verbs: - get +- apiGroups: + - "" + resources: + - persistentvolumeclaims + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - persistentvolumeclaims/status + verbs: + - get +- apiGroups: + - "" + resources: + - persistentvolumes + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - persistentvolumes/status + verbs: + - get - apiGroups: - "" resources: diff --git a/pkg/controllers/gameserver/gameserver_conditions.go b/pkg/controllers/gameserver/gameserver_conditions.go new file mode 100644 index 00000000..8638dcb9 --- /dev/null +++ b/pkg/controllers/gameserver/gameserver_conditions.go @@ -0,0 +1,373 @@ +/* +Copyright 2023 The Kruise 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. +*/ + +package gameserver + +import ( + "context" + "fmt" + gamekruiseiov1alpha1 "github.com/openkruise/kruise-game/apis/v1alpha1" + "github.com/openkruise/kruise-game/pkg/util" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + "reflect" + "sigs.k8s.io/controller-runtime/pkg/client" + "strconv" + "strings" +) + +const ( + pvNotFoundReason string = "PersistentVolume Not Found" + pvcNotFoundReason string = "PersistentVolumeClaim Not Found" +) + +func getConditions(ctx context.Context, c client.Client, gs *gamekruiseiov1alpha1.GameServer, eventRecorder record.EventRecorder) ([]gamekruiseiov1alpha1.GameServerCondition, error) { + var gsConditions []gamekruiseiov1alpha1.GameServerCondition + now := metav1.Now() + oldConditions := gs.Status.Conditions + pod := &corev1.Pod{} + err := c.Get(ctx, types.NamespacedName{ + Name: gs.Name, + Namespace: gs.Namespace, + }, pod) + if err != nil { + return nil, err + } + + podCondition := getPodConditions(pod.DeepCopy()) + oldPodCondition := getGsCondition(oldConditions, gamekruiseiov1alpha1.PodNormal) + if !isConditionEqual(podCondition, oldPodCondition) { + podCondition.LastTransitionTime = now + if podCondition.Status == corev1.ConditionFalse { + eventRecorder.Event(gs, corev1.EventTypeWarning, podCondition.Reason, podCondition.Message) + } + } else { + podCondition.LastTransitionTime = oldPodCondition.LastTransitionTime + } + gsConditions = append(gsConditions, podCondition) + + if pod.Spec.NodeName != "" { + node := &corev1.Node{} + err = c.Get(ctx, types.NamespacedName{ + Name: pod.Spec.NodeName, + }, node) + if err != nil { + return nil, err + } + nodeCondition := getNodeConditions(node.DeepCopy()) + oldNodeCondition := getGsCondition(oldConditions, gamekruiseiov1alpha1.NodeNormal) + if !isConditionEqual(nodeCondition, oldNodeCondition) { + nodeCondition.LastTransitionTime = now + if nodeCondition.Status == corev1.ConditionFalse { + eventRecorder.Event(gs, corev1.EventTypeWarning, nodeCondition.Reason, nodeCondition.Message) + } + } else { + nodeCondition.LastTransitionTime = oldNodeCondition.LastTransitionTime + } + gsConditions = append(gsConditions, nodeCondition) + } + + var pvs []*corev1.PersistentVolume + var volumeNotFoundConditions []gamekruiseiov1alpha1.GameServerCondition + for _, volume := range pod.Spec.Volumes { + if volume.PersistentVolumeClaim != nil { + // get pvc + pvc := &corev1.PersistentVolumeClaim{} + err = c.Get(ctx, types.NamespacedName{ + Name: volume.PersistentVolumeClaim.ClaimName, + Namespace: gs.Namespace, + }, pvc) + if err != nil { + if errors.IsNotFound(err) { + volumeNotFoundConditions = append(volumeNotFoundConditions, pvcNotFoundCondition(gs.Namespace, pvc.Name)) + continue + } + return nil, err + } + + // get pv + pvName := pvc.Spec.VolumeName + if pvName == "" { + volumeNotFoundConditions = append(volumeNotFoundConditions, pvNotFoundCondition(gs.Namespace, pvc.Name)) + continue + } + pv := &corev1.PersistentVolume{} + err = c.Get(ctx, types.NamespacedName{ + Name: pvName, + }, pv) + if err != nil { + if errors.IsNotFound(err) { + volumeNotFoundConditions = append(volumeNotFoundConditions, pvNotFoundCondition(gs.Namespace, pvc.Name)) + continue + } + return nil, err + } + pvs = append(pvs, pv) + } + } + pvCondition := polyCondition(append(volumeNotFoundConditions, getPersistentVolumeConditions(pvs))) + oldPvCondition := getGsCondition(oldConditions, gamekruiseiov1alpha1.PersistentVolumeNormal) + if !isConditionEqual(pvCondition, oldPvCondition) { + pvCondition.LastTransitionTime = now + if pvCondition.Status == corev1.ConditionFalse { + eventRecorder.Event(gs, corev1.EventTypeWarning, pvCondition.Reason, pvCondition.Message) + } + } else { + pvCondition.LastTransitionTime = oldPvCondition.LastTransitionTime + } + gsConditions = append(gsConditions, pvCondition) + + return gsConditions, nil +} + +func getPodConditions(pod *corev1.Pod) gamekruiseiov1alpha1.GameServerCondition { + var message string + var reason string + + // pod status + if pod.Status.Reason != "" { + message, reason = polyMessageReason(message, reason, pod.Status.Message, pod.Status.Reason) + } + + // pod conditions + for _, condition := range pod.Status.Conditions { + switch condition.Type { + case corev1.PodScheduled, corev1.PodInitialized, corev1.ContainersReady: + if condition.Status != corev1.ConditionTrue { + message, reason = polyMessageReason(message, reason, condition.Message, condition.Reason) + } + case corev1.PodReady: + _, containersReadyCondition := util.GetPodConditionFromList(pod.Status.Conditions, corev1.ContainersReady) + if containersReadyCondition != nil && containersReadyCondition.Status == corev1.ConditionTrue && condition.Status == corev1.ConditionFalse { + message, reason = polyMessageReason(message, reason, condition.Message, condition.Reason) + } + } + } + + // containers status + initContainerMessage, initContainerReason := getContainerStatusMessageReason(pod.Status.InitContainerStatuses) + if initContainerMessage != "" && initContainerReason != "" { + message, reason = polyMessageReason(message, reason, initContainerMessage, initContainerReason) + } + containerMessage, containerReason := getContainerStatusMessageReason(pod.Status.ContainerStatuses) + if containerMessage != "" && containerReason != "" { + message, reason = polyMessageReason(message, reason, containerMessage, containerReason) + } + + if message == "" && reason == "" { + return gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.PodNormal, + Status: corev1.ConditionTrue, + LastProbeTime: metav1.Now(), + } + } + + return gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.PodNormal, + Status: corev1.ConditionFalse, + Reason: reason, + Message: message, + LastProbeTime: metav1.Now(), + } +} + +func getContainerStatusMessageReason(containerStatus []corev1.ContainerStatus) (string, string) { + var message string + var reason string + for _, status := range containerStatus { + if status.State.Waiting != nil && status.State.Waiting.Reason != "" { + // get Waiting state reason + message, reason = polyMessageReason(message, reason, status.State.Waiting.Message, status.State.Waiting.Reason) + } + if status.State.Terminated != nil && status.State.Terminated.Reason != "" { + // get Terminated state reason + newMessage := status.State.Terminated.Message + " ExitCode: " + strconv.FormatInt(int64(status.State.Terminated.ExitCode), 10) + newReason := "ContainerTerminated:" + status.State.Terminated.Reason + message, reason = polyMessageReason(message, reason, newMessage, newReason) + } else if status.LastTerminationState.Terminated != nil && status.LastTerminationState.Terminated.Reason != "" && status.State.Running == nil { + // get LastTerminated state reason + newMessage := status.LastTerminationState.Terminated.Message + " ExitCode: " + strconv.FormatInt(int64(status.LastTerminationState.Terminated.ExitCode), 10) + newReason := "ContainerTerminated:" + status.LastTerminationState.Terminated.Reason + message, reason = polyMessageReason(message, reason, newMessage, newReason) + } + } + return message, reason +} + +func getNodeConditions(node *corev1.Node) gamekruiseiov1alpha1.GameServerCondition { + var message string + var reason string + + for _, condition := range node.Status.Conditions { + switch condition.Type { + case corev1.NodeReady: + if condition.Status != corev1.ConditionTrue { + message, reason = polyMessageReason(message, reason, condition.Message, string(condition.Type)+":"+condition.Reason) + } + default: + if condition.Status != corev1.ConditionFalse { + message, reason = polyMessageReason(message, reason, condition.Message, string(condition.Type)+":"+condition.Reason) + } + } + } + + if message == "" && reason == "" { + return gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.NodeNormal, + Status: corev1.ConditionTrue, + LastProbeTime: metav1.Now(), + } + } + + return gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.NodeNormal, + Status: corev1.ConditionFalse, + Reason: reason, + Message: message, + LastProbeTime: metav1.Now(), + } +} + +func getPersistentVolumeConditions(pvs []*corev1.PersistentVolume) gamekruiseiov1alpha1.GameServerCondition { + var message string + var reason string + + for _, pv := range pvs { + if pv.Status.Reason != "" { + message, reason = polyMessageReason(message, reason, pv.Status.Message, pv.Status.Reason) + } + } + + if message == "" && reason == "" { + return gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.PersistentVolumeNormal, + Status: corev1.ConditionTrue, + LastProbeTime: metav1.Now(), + } + } + + return gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.PersistentVolumeNormal, + Status: corev1.ConditionFalse, + Reason: reason, + Message: message, + LastProbeTime: metav1.Now(), + } +} + +func pvcNotFoundCondition(namespace, pvcName string) gamekruiseiov1alpha1.GameServerCondition { + return gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.PersistentVolumeNormal, + Status: corev1.ConditionFalse, + Reason: pvcNotFoundReason, + Message: fmt.Sprintf("There is no pvc named %s/%s in cluster", namespace, pvcName), + LastProbeTime: metav1.Now(), + } +} + +func pvNotFoundCondition(namespace, pvcName string) gamekruiseiov1alpha1.GameServerCondition { + return gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.PersistentVolumeNormal, + Status: corev1.ConditionFalse, + Reason: pvNotFoundReason, + Message: fmt.Sprintf("There is no pv which pvc %s/%s is bound with", namespace, pvcName), + LastProbeTime: metav1.Now(), + } +} + +func polyCondition(conditions []gamekruiseiov1alpha1.GameServerCondition) gamekruiseiov1alpha1.GameServerCondition { + // remove null conditions + var noNullConditions []gamekruiseiov1alpha1.GameServerCondition + for _, condition := range conditions { + if !reflect.DeepEqual(condition, gamekruiseiov1alpha1.GameServerCondition{}) { + noNullConditions = append(noNullConditions, condition) + } + } + + ret := gamekruiseiov1alpha1.GameServerCondition{} + isAllTrue := true + for _, condition := range noNullConditions { + if condition.Status == corev1.ConditionFalse { + isAllTrue = false + break + } + } + for _, condition := range noNullConditions { + if !isAllTrue && condition.Status == corev1.ConditionTrue { + continue + } + if reflect.DeepEqual(ret, gamekruiseiov1alpha1.GameServerCondition{}) { + ret = condition + } else { + if condition.Type != ret.Type { + continue + } + ret.Message, ret.Reason = polyMessageReason(ret.Message, ret.Reason, condition.Message, condition.Reason) + } + } + return ret +} + +func polyMessageReason(message, reason string, newMessage, newReason string) (string, string) { + if message == "" && reason == "" { + return newMessage, newReason + } + var retMessage string + var retReason string + if strings.Contains(reason, newReason) { + retReason = reason + } else { + retReason = reason + "; " + newReason + } + if strings.Contains(message, newMessage) { + retMessage = message + } else { + retMessage = message + "; " + newMessage + } + + return retMessage, retReason +} + +func getGsCondition(conditions []gamekruiseiov1alpha1.GameServerCondition, conditionType gamekruiseiov1alpha1.GameServerConditionType) gamekruiseiov1alpha1.GameServerCondition { + if conditions == nil { + return gamekruiseiov1alpha1.GameServerCondition{} + } + for _, condition := range conditions { + if condition.Type == conditionType { + return condition + } + } + return gamekruiseiov1alpha1.GameServerCondition{} +} + +func isConditionEqual(a, b gamekruiseiov1alpha1.GameServerCondition) bool { + if a.Type != b.Type { + return false + } + if a.Status != b.Status { + return false + } + if a.Message != b.Message { + return false + } + if a.Reason != b.Reason { + return false + } + return true +} diff --git a/pkg/controllers/gameserver/gameserver_conditions_test.go b/pkg/controllers/gameserver/gameserver_conditions_test.go new file mode 100644 index 00000000..9628340c --- /dev/null +++ b/pkg/controllers/gameserver/gameserver_conditions_test.go @@ -0,0 +1,750 @@ +package gameserver + +import ( + gamekruiseiov1alpha1 "github.com/openkruise/kruise-game/apis/v1alpha1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "reflect" + "testing" +) + +func TestPolyMessageReason(t *testing.T) { + tests := []struct { + message string + reason string + newMessage string + newReason string + resultMessage string + resultReason string + }{ + // case 0 + { + message: "", + reason: "reason_0", + newMessage: "message_1", + newReason: "reason_1", + resultMessage: "; message_1", + resultReason: "reason_0; reason_1", + }, + // case 1 + { + message: "message_0", + reason: "", + newMessage: "message_1", + newReason: "reason_1", + resultMessage: "message_0; message_1", + resultReason: "; reason_1", + }, + // case 2 + { + message: "", + reason: "", + newMessage: "message_1", + newReason: "reason_1", + resultMessage: "message_1", + resultReason: "reason_1", + }, + // case 3 + { + message: "message_0", + reason: "reason_0", + newMessage: "message_1", + newReason: "reason_1", + resultMessage: "message_0; message_1", + resultReason: "reason_0; reason_1", + }, + // case 4 + { + message: "NodeStatusUnknown", + reason: "Kubelet stopped posting node status.", + newMessage: "NodeStatusUnknown", + newReason: "Kubelet stopped posting node status.", + resultMessage: "NodeStatusUnknown", + resultReason: "Kubelet stopped posting node status.", + }, + // case 5 + { + reason: "MemoryPressure:NodeStatusUnknown", + message: "Kubelet stopped posting node status.", + newReason: "PIDPressure:NodeStatusUnknown", + newMessage: "Kubelet stopped posting node status.", + resultReason: "MemoryPressure:NodeStatusUnknown; PIDPressure:NodeStatusUnknown", + resultMessage: "Kubelet stopped posting node status.", + }, + } + + for i, test := range tests { + actualMessage, actualReason := polyMessageReason(test.message, test.reason, test.newMessage, test.newReason) + if test.resultMessage != actualMessage { + t.Errorf("case %d: expect message is %s, but actually is %s", i, test.resultMessage, actualMessage) + } + if test.resultReason != actualReason { + t.Errorf("case %d: expect reason is %s, but actually is %s", i, test.resultReason, actualReason) + } + } +} + +func TestPolyCondition(t *testing.T) { + tests := []struct { + before []gamekruiseiov1alpha1.GameServerCondition + after gamekruiseiov1alpha1.GameServerCondition + }{ + // case 0 + { + before: []gamekruiseiov1alpha1.GameServerCondition{ + { + Type: gamekruiseiov1alpha1.PersistentVolumeNormal, + Status: corev1.ConditionFalse, + Message: "message_0", + Reason: "reason_0", + }, + { + Type: gamekruiseiov1alpha1.PersistentVolumeNormal, + Status: corev1.ConditionFalse, + Message: "message_1", + Reason: "reason_1", + }, + }, + after: gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.PersistentVolumeNormal, + Status: corev1.ConditionFalse, + Message: "message_0; message_1", + Reason: "reason_0; reason_1", + }, + }, + // case 1 + { + before: []gamekruiseiov1alpha1.GameServerCondition{ + { + Type: gamekruiseiov1alpha1.PersistentVolumeNormal, + Status: corev1.ConditionFalse, + Message: "message_0", + Reason: "reason_0", + }, + { + Type: gamekruiseiov1alpha1.PodNormal, + Status: corev1.ConditionFalse, + Message: "message_1", + Reason: "reason_1", + }, + }, + after: gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.PersistentVolumeNormal, + Status: corev1.ConditionFalse, + Message: "message_0", + Reason: "reason_0", + }, + }, + // case 2 + { + before: []gamekruiseiov1alpha1.GameServerCondition{ + { + Type: gamekruiseiov1alpha1.PersistentVolumeNormal, + Status: corev1.ConditionTrue, + Message: "message_0", + Reason: "reason_0", + }, + { + Type: gamekruiseiov1alpha1.PodNormal, + Status: corev1.ConditionFalse, + Message: "message_1", + Reason: "reason_1", + }, + }, + after: gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.PodNormal, + Status: corev1.ConditionFalse, + Message: "message_1", + Reason: "reason_1", + }, + }, + // case 3 + { + before: []gamekruiseiov1alpha1.GameServerCondition{}, + after: gamekruiseiov1alpha1.GameServerCondition{}, + }, + // case 4 + { + before: []gamekruiseiov1alpha1.GameServerCondition{ + { + Type: gamekruiseiov1alpha1.PersistentVolumeNormal, + Status: corev1.ConditionTrue, + Message: "message_0", + Reason: "reason_0", + }, + { + Type: gamekruiseiov1alpha1.PersistentVolumeNormal, + Status: corev1.ConditionTrue, + Message: "message_1", + Reason: "reason_1", + }, + }, + after: gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.PersistentVolumeNormal, + Status: corev1.ConditionTrue, + Message: "message_0; message_1", + Reason: "reason_0; reason_1", + }, + }, + } + + for i, test := range tests { + actual := polyCondition(test.before) + test.after.LastProbeTime = actual.LastProbeTime + test.after.LastTransitionTime = actual.LastTransitionTime + if !reflect.DeepEqual(test.after, actual) { + t.Errorf("case %d: expect condition is %v, but actually is %v", i, test.after, actual) + } + } +} + +func TestPvNotFoundCondition(t *testing.T) { + tests := []struct { + pvcName string + namespace string + condition gamekruiseiov1alpha1.GameServerCondition + }{ + { + pvcName: "pvc_0", + namespace: "default", + condition: gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.PersistentVolumeNormal, + Status: corev1.ConditionFalse, + Reason: pvNotFoundReason, + Message: "There is no pv which pvc default/pvc_0 is bound with", + }, + }, + } + + for i, test := range tests { + actual := pvNotFoundCondition(test.namespace, test.pvcName) + test.condition.LastProbeTime = actual.LastProbeTime + test.condition.LastTransitionTime = actual.LastTransitionTime + if !reflect.DeepEqual(test.condition, actual) { + t.Errorf("case %d: expect condition is %v ,but actually is %v", i, test.condition, actual) + } + } +} + +func TestPvcNotFoundCondition(t *testing.T) { + tests := []struct { + pvcName string + namespace string + condition gamekruiseiov1alpha1.GameServerCondition + }{ + { + pvcName: "pvc_0", + namespace: "default", + condition: gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.PersistentVolumeNormal, + Status: corev1.ConditionFalse, + Reason: pvcNotFoundReason, + Message: "There is no pvc named default/pvc_0 in cluster", + }, + }, + } + + for i, test := range tests { + actual := pvcNotFoundCondition(test.namespace, test.pvcName) + test.condition.LastProbeTime = actual.LastProbeTime + test.condition.LastTransitionTime = actual.LastTransitionTime + if !reflect.DeepEqual(test.condition, actual) { + t.Errorf("case %d: expect condition is %v ,but actually is %v", i, test.condition, actual) + } + } +} + +func TestGetPersistentVolumeConditions(t *testing.T) { + tests := []struct { + pvs []*corev1.PersistentVolume + condition gamekruiseiov1alpha1.GameServerCondition + }{ + // case 0 + { + pvs: []*corev1.PersistentVolume{ + { + Status: corev1.PersistentVolumeStatus{ + Message: "message_0", + Reason: "reason_0", + }, + }, + { + Status: corev1.PersistentVolumeStatus{ + Message: "message_1", + Reason: "reason_1", + }, + }, + }, + condition: gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.PersistentVolumeNormal, + Status: corev1.ConditionFalse, + Message: "message_0; message_1", + Reason: "reason_0; reason_1", + }, + }, + // case 1 + { + pvs: []*corev1.PersistentVolume{ + { + Status: corev1.PersistentVolumeStatus{ + Message: "message_0", + Reason: "reason_0", + }, + }, + { + Status: corev1.PersistentVolumeStatus{ + Message: "", + Reason: "", + }, + }, + }, + condition: gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.PersistentVolumeNormal, + Status: corev1.ConditionFalse, + Message: "message_0", + Reason: "reason_0", + }, + }, + // case 2 + { + pvs: []*corev1.PersistentVolume{ + { + Status: corev1.PersistentVolumeStatus{ + Message: "", + Reason: "", + }, + }, + { + Status: corev1.PersistentVolumeStatus{ + Message: "", + Reason: "", + }, + }, + }, + condition: gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.PersistentVolumeNormal, + Status: corev1.ConditionTrue, + }, + }, + } + + for i, test := range tests { + actual := getPersistentVolumeConditions(test.pvs) + test.condition.LastProbeTime = actual.LastProbeTime + test.condition.LastTransitionTime = actual.LastTransitionTime + if !reflect.DeepEqual(test.condition, actual) { + t.Errorf("case %d: expect condition is %v, but actually is %v", i, test.condition, actual) + } + } +} + +func TestGetNodeConditions(t *testing.T) { + tests := []struct { + node *corev1.Node + condition gamekruiseiov1alpha1.GameServerCondition + }{ + // case 0 + { + node: &corev1.Node{ + Status: corev1.NodeStatus{ + Conditions: []corev1.NodeCondition{ + { + Type: corev1.NodeReady, + Status: corev1.ConditionTrue, + Reason: "KubeletReady", + Message: "kubelet is posting ready status", + }, + { + Type: corev1.NodeDiskPressure, + Status: corev1.ConditionFalse, + Reason: "KubeletHasNoDiskPressure", + Message: "kubelet has no disk pressure", + }, + }, + }, + }, + condition: gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.NodeNormal, + Status: corev1.ConditionTrue, + }, + }, + // case 1 + { + node: &corev1.Node{ + Status: corev1.NodeStatus{ + Conditions: []corev1.NodeCondition{ + { + Type: corev1.NodeReady, + Status: corev1.ConditionFalse, + Reason: "KubeletNotReady", + Message: "kubelet is not posting ready status", + }, + { + Type: corev1.NodeDiskPressure, + Status: corev1.ConditionTrue, + Reason: "KubeletHasDiskPressure", + Message: "kubelet has disk pressure", + }, + }, + }, + }, + condition: gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.NodeNormal, + Status: corev1.ConditionFalse, + Reason: "Ready:KubeletNotReady; DiskPressure:KubeletHasDiskPressure", + Message: "kubelet is not posting ready status; kubelet has disk pressure", + }, + }, + // case 2 + { + node: &corev1.Node{ + Status: corev1.NodeStatus{ + Conditions: []corev1.NodeCondition{ + { + Type: corev1.NodeDiskPressure, + Status: corev1.ConditionUnknown, + Reason: "NodeStatusUnknown", + Message: "Kubelet stopped posting node status.", + }, + { + Type: corev1.NodeMemoryPressure, + Status: corev1.ConditionUnknown, + Reason: "NodeStatusUnknown", + Message: "Kubelet stopped posting node status.", + }, + }, + }, + }, + condition: gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.NodeNormal, + Status: corev1.ConditionFalse, + Reason: "DiskPressure:NodeStatusUnknown; MemoryPressure:NodeStatusUnknown", + Message: "Kubelet stopped posting node status.", + }, + }, + } + + for i, test := range tests { + actual := getNodeConditions(test.node) + test.condition.LastProbeTime = actual.LastProbeTime + test.condition.LastTransitionTime = actual.LastTransitionTime + if !reflect.DeepEqual(test.condition, actual) { + t.Errorf("case %d: expect condition is %v, but actually is %v", i, test.condition, actual) + } + } +} + +func TestGetPodConditions(t *testing.T) { + tests := []struct { + pod *corev1.Pod + condition gamekruiseiov1alpha1.GameServerCondition + }{ + // case 0 + { + pod: &corev1.Pod{ + Status: corev1.PodStatus{ + Conditions: []corev1.PodCondition{ + { + Type: corev1.PodScheduled, + Status: corev1.ConditionFalse, + Reason: "Reason_0", + Message: "Message_0", + }, + }, + }, + }, + condition: gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.PodNormal, + Status: corev1.ConditionFalse, + Reason: "Reason_0", + Message: "Message_0", + }, + }, + // case 1 + { + pod: &corev1.Pod{ + Status: corev1.PodStatus{ + Conditions: []corev1.PodCondition{ + { + Type: corev1.ContainersReady, + Status: corev1.ConditionTrue, + Reason: "", + Message: "", + }, + { + Type: corev1.PodReady, + Status: corev1.ConditionFalse, + Reason: "Readiness Failed", + Message: "container game-server readiness probe failed", + }, + }, + }, + }, + condition: gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.PodNormal, + Status: corev1.ConditionFalse, + Reason: "Readiness Failed", + Message: "container game-server readiness probe failed", + }, + }, + // case 2 + { + pod: &corev1.Pod{ + Status: corev1.PodStatus{ + ContainerStatuses: []corev1.ContainerStatus{ + { + State: corev1.ContainerState{ + Terminated: &corev1.ContainerStateTerminated{ + ExitCode: 137, + Message: "message_0", + Reason: "reason_0", + }, + }, + }, + }, + Conditions: []corev1.PodCondition{ + { + Type: corev1.ContainersReady, + Status: corev1.ConditionFalse, + Reason: "reason_1", + Message: "message_1", + }, + { + Type: corev1.PodReady, + Status: corev1.ConditionFalse, + Reason: "reason_2", + Message: "message_2", + }, + }, + }, + }, + condition: gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.PodNormal, + Status: corev1.ConditionFalse, + Reason: "reason_1; ContainerTerminated:reason_0", + Message: "message_1; message_0 ExitCode: 137", + }, + }, + // case 3 + { + pod: &corev1.Pod{ + Status: corev1.PodStatus{ + InitContainerStatuses: []corev1.ContainerStatus{ + { + State: corev1.ContainerState{ + Waiting: &corev1.ContainerStateWaiting{ + Message: "message_0", + Reason: "reason_0", + }, + }, + }, + }, + Conditions: []corev1.PodCondition{ + { + Type: corev1.PodInitialized, + Status: corev1.ConditionFalse, + Reason: "reason_1", + Message: "message_1", + }, + { + Type: corev1.PodReady, + Status: corev1.ConditionFalse, + Reason: "reason_2", + Message: "message_2", + }, + }, + }, + }, + condition: gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.PodNormal, + Status: corev1.ConditionFalse, + Reason: "reason_1; reason_0", + Message: "message_1; message_0", + }, + }, + // case 4 + { + pod: &corev1.Pod{ + Status: corev1.PodStatus{ + Conditions: []corev1.PodCondition{ + { + Type: corev1.PodScheduled, + Status: corev1.ConditionTrue, + Reason: "", + Message: "", + }, + }, + }, + }, + condition: gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.PodNormal, + Status: corev1.ConditionTrue, + }, + }, + } + + for i, test := range tests { + actual := getPodConditions(test.pod) + test.condition.LastProbeTime = actual.LastProbeTime + test.condition.LastTransitionTime = actual.LastTransitionTime + if !reflect.DeepEqual(test.condition, actual) { + t.Errorf("case %d: expect condition is %v, but actually is %v", i, test.condition, actual) + } + } +} + +func TestIsConditionEqual(t *testing.T) { + tests := []struct { + a gamekruiseiov1alpha1.GameServerCondition + b gamekruiseiov1alpha1.GameServerCondition + result bool + }{ + { + a: gamekruiseiov1alpha1.GameServerCondition{}, + b: gamekruiseiov1alpha1.GameServerCondition{}, + result: true, + }, + { + a: gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.PodNormal, + Status: corev1.ConditionFalse, + }, + b: gamekruiseiov1alpha1.GameServerCondition{}, + result: false, + }, + { + a: gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.PodNormal, + Status: corev1.ConditionFalse, + }, + b: gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.PodNormal, + Status: corev1.ConditionFalse, + }, + result: true, + }, + { + a: gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.PodNormal, + Status: corev1.ConditionFalse, + Message: "xxx", + }, + b: gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.PodNormal, + Status: corev1.ConditionFalse, + Reason: "xxx", + }, + result: false, + }, + { + a: gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.PodNormal, + Status: corev1.ConditionFalse, + Reason: "xxx", + }, + b: gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.PodNormal, + Status: corev1.ConditionFalse, + Reason: "xxx", + }, + result: true, + }, + { + a: gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.PodNormal, + Status: corev1.ConditionFalse, + Reason: "", + }, + b: gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.PodNormal, + Status: corev1.ConditionFalse, + }, + result: true, + }, + { + a: gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.PodNormal, + Status: corev1.ConditionTrue, + LastProbeTime: metav1.Now(), + }, + b: gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.PodNormal, + Status: corev1.ConditionTrue, + LastProbeTime: metav1.Now(), + }, + result: true, + }, + } + + for i, test := range tests { + actual := isConditionEqual(test.a, test.b) + if test.result != actual { + t.Errorf("case %d: expect result is %v, but actually is %v", i, test.result, actual) + } + } +} + +func TestGetGsCondition(t *testing.T) { + tests := []struct { + conditions []gamekruiseiov1alpha1.GameServerCondition + conditionType gamekruiseiov1alpha1.GameServerConditionType + result gamekruiseiov1alpha1.GameServerCondition + }{ + // case 0 + { + conditions: []gamekruiseiov1alpha1.GameServerCondition{}, + conditionType: gamekruiseiov1alpha1.PodNormal, + result: gamekruiseiov1alpha1.GameServerCondition{}, + }, + // case 1 + { + conditions: []gamekruiseiov1alpha1.GameServerCondition{ + { + Type: gamekruiseiov1alpha1.NodeNormal, + Status: corev1.ConditionFalse, + Message: "message_0", + Reason: "reason_0", + }, + { + Type: gamekruiseiov1alpha1.PodNormal, + Status: corev1.ConditionFalse, + Message: "message_1", + Reason: "reason_1", + }, + }, + conditionType: gamekruiseiov1alpha1.PodNormal, + result: gamekruiseiov1alpha1.GameServerCondition{ + Type: gamekruiseiov1alpha1.PodNormal, + Status: corev1.ConditionFalse, + Message: "message_1", + Reason: "reason_1", + }, + }, + // case 2 + { + conditions: []gamekruiseiov1alpha1.GameServerCondition{ + { + Type: gamekruiseiov1alpha1.NodeNormal, + Status: corev1.ConditionFalse, + Message: "message_0", + Reason: "reason_0", + }, + { + Type: gamekruiseiov1alpha1.PodNormal, + Status: corev1.ConditionFalse, + Message: "message_1", + Reason: "reason_1", + }, + }, + conditionType: gamekruiseiov1alpha1.PersistentVolumeNormal, + result: gamekruiseiov1alpha1.GameServerCondition{}, + }, + } + + for i, test := range tests { + actual := getGsCondition(test.conditions, test.conditionType) + if test.result != actual { + t.Errorf("case %d: expect condition is %v, but actually is %v", i, test.result, actual) + } + } +} diff --git a/pkg/controllers/gameserver/gameserver_manager.go b/pkg/controllers/gameserver/gameserver_manager.go index c6f02c98..d49cf590 100644 --- a/pkg/controllers/gameserver/gameserver_manager.go +++ b/pkg/controllers/gameserver/gameserver_manager.go @@ -220,6 +220,13 @@ func (manager GameServerManager) SyncPodToGs(gss *gameKruiseV1alpha1.GameServerS return err } + // get gs conditions + conditions, err := getConditions(context.TODO(), manager.client, gs, manager.eventRecorder) + if err != nil { + klog.Errorf("failed to get GameServer %s Conditions in %s, because of %s.", gs.GetName(), gs.GetNamespace(), err.Error()) + return err + } + // patch gs status status := gameKruiseV1alpha1.GameServerStatus{ PodStatus: pod.Status, @@ -230,6 +237,7 @@ func (manager GameServerManager) SyncPodToGs(gss *gameKruiseV1alpha1.GameServerS ServiceQualitiesCondition: newGsConditions, NetworkStatus: manager.syncNetworkStatus(), LastTransitionTime: metav1.Now(), + Conditions: conditions, } patchStatus := map[string]interface{}{"status": status} jsonPatchStatus, err := json.Marshal(patchStatus) diff --git a/pkg/webhook/webhook.go b/pkg/webhook/webhook.go index cb816b57..0c999105 100644 --- a/pkg/webhook/webhook.go +++ b/pkg/webhook/webhook.go @@ -67,6 +67,10 @@ func init() { // +kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses/status,verbs=get;update;patch // +kubebuilder:rbac:groups=core,resources=nodes,verbs=get;list;watch // +kubebuilder:rbac:groups=core,resources=nodes/status,verbs=get +// +kubebuilder:rbac:groups=core,resources=persistentvolumeclaims,verbs=get;list;watch +// +kubebuilder:rbac:groups=core,resources=persistentvolumeclaims/status,verbs=get +// +kubebuilder:rbac:groups=core,resources=persistentvolumes,verbs=get;list;watch +// +kubebuilder:rbac:groups=core,resources=persistentvolumes/status,verbs=get // +kubebuilder:rbac:groups=admissionregistration.k8s.io,resources=mutatingwebhookconfigurations,verbs=create;get;list;watch;update;patch // +kubebuilder:rbac:groups=admissionregistration.k8s.io,resources=validatingwebhookconfigurations,verbs=create;get;list;watch;update;patch // +kubebuilder:rbac:groups=apiextensions.k8s.io,resources=customresourcedefinitions,verbs=get;list;watch;update;patch