diff --git a/apis/v1alpha1/gameserver_types.go b/apis/v1alpha1/gameserver_types.go index bc29fd06..7246c690 100644 --- a/apis/v1alpha1/gameserver_types.go +++ b/apis/v1alpha1/gameserver_types.go @@ -41,6 +41,18 @@ type GameServerSpec struct { UpdatePriority *intstr.IntOrString `json:"updatePriority,omitempty"` DeletionPriority *intstr.IntOrString `json:"deletionPriority,omitempty"` NetworkDisabled bool `json:"networkDisabled,omitempty"` + // Containers can be used to make the corresponding GameServer container fields + // different from the fields defined by GameServerTemplate in GameServerSetSpec. + Containers []GameServerContainer `json:"containers,omitempty"` +} + +type GameServerContainer struct { + // Name indicates the name of the container to update. + Name string `json:"name"` + // Image indicates the image of the container to update. + Image string `json:"image,omitempty"` + // Resources indicates the resources of the container to update. + Resources corev1.ResourceRequirements `json:"resources,omitempty"` } type GameServerState string diff --git a/apis/v1alpha1/zz_generated.deepcopy.go b/apis/v1alpha1/zz_generated.deepcopy.go index 5f93d076..fa43487d 100644 --- a/apis/v1alpha1/zz_generated.deepcopy.go +++ b/apis/v1alpha1/zz_generated.deepcopy.go @@ -72,6 +72,22 @@ func (in *GameServerCondition) DeepCopy() *GameServerCondition { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GameServerContainer) DeepCopyInto(out *GameServerContainer) { + *out = *in + in.Resources.DeepCopyInto(&out.Resources) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GameServerContainer. +func (in *GameServerContainer) DeepCopy() *GameServerContainer { + if in == nil { + return nil + } + out := new(GameServerContainer) + 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 @@ -241,6 +257,13 @@ func (in *GameServerSpec) DeepCopyInto(out *GameServerSpec) { *out = new(intstr.IntOrString) **out = **in } + if in.Containers != nil { + in, out := &in.Containers, &out.Containers + *out = make([]GameServerContainer, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GameServerSpec. diff --git a/config/crd/bases/game.kruise.io_gameservers.yaml b/config/crd/bases/game.kruise.io_gameservers.yaml index 590167ee..e3c5b59a 100644 --- a/config/crd/bases/game.kruise.io_gameservers.yaml +++ b/config/crd/bases/game.kruise.io_gameservers.yaml @@ -58,6 +58,50 @@ spec: spec: description: GameServerSpec defines the desired state of GameServer properties: + containers: + description: Containers can be used to make the corresponding GameServer + container fields different from the fields defined by GameServerTemplate + in GameServerSetSpec. + items: + properties: + image: + description: Image indicates the image of the container to update. + type: string + name: + description: Name indicates the name of the container to update. + type: string + resources: + description: Resources indicates the resources of the container + to update. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Limits describes the maximum amount of compute + resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Requests describes the minimum amount of compute + resources required. If Requests is omitted for a container, + it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. More info: + https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + type: object + required: + - name + type: object + type: array deletionPriority: anyOf: - type: integer diff --git a/config/crd/bases/game.kruise.io_gameserversets.yaml b/config/crd/bases/game.kruise.io_gameserversets.yaml index 12d1cce5..0ce37ca5 100644 --- a/config/crd/bases/game.kruise.io_gameserversets.yaml +++ b/config/crd/bases/game.kruise.io_gameserversets.yaml @@ -535,6 +535,53 @@ spec: serviceQualityAction: items: properties: + containers: + description: Containers can be used to make the corresponding + GameServer container fields different from the fields + defined by GameServerTemplate in GameServerSetSpec. + items: + properties: + image: + description: Image indicates the image of the container + to update. + type: string + name: + description: Name indicates the name of the container + to update. + type: string + resources: + description: Resources indicates the resources of + the container to update. + properties: + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Limits describes the maximum amount + of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Requests describes the minimum + amount of compute resources required. If Requests + is omitted for a container, it defaults to + Limits if that is explicitly specified, otherwise + to an implementation-defined value. More info: + https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + type: object + required: + - name + type: object + type: array deletionPriority: anyOf: - type: integer diff --git a/pkg/webhook/mutating_pod.go b/pkg/webhook/mutating_pod.go index d93dc9af..a2c1750b 100644 --- a/pkg/webhook/mutating_pod.go +++ b/pkg/webhook/mutating_pod.go @@ -20,10 +20,14 @@ import ( "context" "encoding/json" "fmt" + gameKruiseV1alpha1 "github.com/openkruise/kruise-game/apis/v1alpha1" "github.com/openkruise/kruise-game/cloudprovider/errors" "github.com/openkruise/kruise-game/cloudprovider/manager" admissionv1 "k8s.io/api/admission/v1" corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/record" "k8s.io/klog/v2" "net/http" @@ -56,11 +60,20 @@ func (pmh *PodMutatingHandler) Handle(ctx context.Context, req admission.Request return admission.Errored(http.StatusInternalServerError, err) } + if req.Operation == admissionv1.Create { + pod, err = patchContainers(pmh.Client, pod, ctx) + if err != nil { + msg := fmt.Sprintf("Pod %s/%s patchContainers failed, because of %s", pod.Namespace, pod.Name, err.Error()) + return admission.Denied(msg) + } + } + // get the plugin according to pod plugin, ok := pmh.CloudProviderManager.FindAvailablePlugins(pod) if !ok { msg := fmt.Sprintf("Pod %s/%s has no available plugin", pod.Namespace, pod.Name) - return admission.Allowed(msg) + klog.Infof(msg) + return getAdmissionResponse(req, patchResult{pod: pod, err: nil}) } // define context with timeout @@ -136,3 +149,63 @@ func NewPodMutatingHandler(client client.Client, decoder *admission.Decoder, cpm eventRecorder: recorder, } } + +func patchContainers(client client.Client, pod *corev1.Pod, ctx context.Context) (*corev1.Pod, error) { + if _, ok := pod.GetLabels()[gameKruiseV1alpha1.GameServerOwnerGssKey]; !ok { + return pod, nil + } + gs := &gameKruiseV1alpha1.GameServer{} + err := client.Get(ctx, types.NamespacedName{ + Namespace: pod.GetNamespace(), + Name: pod.GetName(), + }, gs) + if err != nil { + if k8serrors.IsNotFound(err) { + return pod, nil + } + return pod, err + } + if gs.Spec.Containers != nil { + var containers []corev1.Container + for _, podContainer := range pod.Spec.Containers { + container := podContainer + for _, gsContainer := range gs.Spec.Containers { + if gsContainer.Name == podContainer.Name { + // patch Image + if gsContainer.Image != podContainer.Image { + container.Image = gsContainer.Image + } + + // patch Resources + if limitCPU, ok := gsContainer.Resources.Limits[corev1.ResourceCPU]; ok { + if container.Resources.Limits == nil { + container.Resources.Limits = make(map[corev1.ResourceName]resource.Quantity) + } + container.Resources.Limits[corev1.ResourceCPU] = limitCPU + } + if limitMemory, ok := gsContainer.Resources.Limits[corev1.ResourceMemory]; ok { + if container.Resources.Limits == nil { + container.Resources.Limits = make(map[corev1.ResourceName]resource.Quantity) + } + container.Resources.Limits[corev1.ResourceMemory] = limitMemory + } + if requestCPU, ok := gsContainer.Resources.Requests[corev1.ResourceCPU]; ok { + if container.Resources.Requests == nil { + container.Resources.Requests = make(map[corev1.ResourceName]resource.Quantity) + } + container.Resources.Requests[corev1.ResourceCPU] = requestCPU + } + if requestMemory, ok := gsContainer.Resources.Requests[corev1.ResourceMemory]; ok { + if container.Resources.Requests == nil { + container.Resources.Requests = make(map[corev1.ResourceName]resource.Quantity) + } + container.Resources.Requests[corev1.ResourceMemory] = requestMemory + } + } + } + containers = append(containers, container) + } + pod.Spec.Containers = containers + } + return pod, nil +} diff --git a/pkg/webhook/mutating_pod_test.go b/pkg/webhook/mutating_pod_test.go new file mode 100644 index 00000000..f0ef7504 --- /dev/null +++ b/pkg/webhook/mutating_pod_test.go @@ -0,0 +1,137 @@ +package webhook + +import ( + "context" + gameKruiseV1alpha1 "github.com/openkruise/kruise-game/apis/v1alpha1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "reflect" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "testing" +) + +var ( + scheme = runtime.NewScheme() +) + +func init() { + utilruntime.Must(gameKruiseV1alpha1.AddToScheme(scheme)) + utilruntime.Must(corev1.AddToScheme(scheme)) +} + +func TestPatchContainers(t *testing.T) { + tests := []struct { + gs *gameKruiseV1alpha1.GameServer + oldPod *corev1.Pod + newContainers []corev1.Container + }{ + // case 0 + { + gs: nil, + oldPod: &corev1.Pod{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "A", + Image: "A-v1", + }, + }, + }, + }, + newContainers: []corev1.Container{ + { + Name: "A", + Image: "A-v1", + }, + }, + }, + + // case 1 + { + gs: &gameKruiseV1alpha1.GameServer{ + Spec: gameKruiseV1alpha1.GameServerSpec{ + Containers: []gameKruiseV1alpha1.GameServerContainer{ + { + Name: "A", + Image: "A-v2", + Resources: corev1.ResourceRequirements{ + Requests: map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceMemory: resource.MustParse("2Gi"), + }, + Limits: map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceCPU: resource.MustParse("1"), + }, + }, + }, + }, + }, + }, + oldPod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + gameKruiseV1alpha1.GameServerOwnerGssKey: "xxx", + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "A", + Image: "A-v1", + Resources: corev1.ResourceRequirements{ + Requests: map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceCPU: resource.MustParse("500m"), + corev1.ResourceMemory: resource.MustParse("2Gi"), + }, + }, + }, + { + Name: "B", + Image: "B-v1", + }, + }, + }, + }, + + newContainers: []corev1.Container{ + { + Name: "A", + Image: "A-v2", + Resources: corev1.ResourceRequirements{ + Requests: map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceCPU: resource.MustParse("500m"), + corev1.ResourceMemory: resource.MustParse("2Gi"), + }, + Limits: map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceCPU: resource.MustParse("1"), + }, + }, + }, + { + Name: "B", + Image: "B-v1", + }, + }, + }, + } + + for i, test := range tests { + expect := test.newContainers + var objs []client.Object + if test.gs != nil { + objs = append(objs, test.gs) + } + c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(objs...).Build() + newPod, err := patchContainers(c, test.oldPod, context.Background()) + if err != nil { + t.Error(err) + } + actual := newPod.Spec.Containers + if !reflect.DeepEqual(expect, actual) { + t.Errorf("case %d: expect new containers %v, but actually got %v", i, expect, actual) + } + } +}